diff --git a/app/vendor/adodb/adodb-php/.gitattributes b/app/vendor/adodb/adodb-php/.gitattributes new file mode 100644 index 000000000..2cb098d87 --- /dev/null +++ b/app/vendor/adodb/adodb-php/.gitattributes @@ -0,0 +1,17 @@ +# Default behavior +* text=auto + +# Text files to be normalized to lf line endings +*.php text +*.htm* text +*.sql text +*.txt text +*.xml text +*.xsl text + +# Windows-only files +*.bat eol=crlf + +# Binary files +*.gif binary + diff --git a/app/vendor/adodb/adodb-php/.gitignore b/app/vendor/adodb/adodb-php/.gitignore new file mode 100644 index 000000000..0d777b27e --- /dev/null +++ b/app/vendor/adodb/adodb-php/.gitignore @@ -0,0 +1,10 @@ +# IDE/Editor temporary files +*~ +*.swp +*.bak +*.tmp +.project +.settings/ +/nbproject/ +.idea/ + diff --git a/app/vendor/adodb/adodb-php/.mailmap b/app/vendor/adodb/adodb-php/.mailmap new file mode 100644 index 000000000..1f2f7e7f0 --- /dev/null +++ b/app/vendor/adodb/adodb-php/.mailmap @@ -0,0 +1,4 @@ +Andreas Fernandez +Mike Benoit MikeB +Mike Benoit mike.benoit + diff --git a/app/vendor/adodb/adodb-php/LICENSE.md b/app/vendor/adodb/adodb-php/LICENSE.md new file mode 100644 index 000000000..b8191587b --- /dev/null +++ b/app/vendor/adodb/adodb-php/LICENSE.md @@ -0,0 +1,496 @@ +ADOdb License +============= + +ADOdb is dual licensed under BSD and LGPL. + +In plain English, you do not need to distribute your application in source code form, +nor do you need to distribute ADOdb source code, provided you follow the rest of +terms of the BSD license. + +For more info about ADOdb, visit http://adodb.sourceforge.net/ + +BSD 3-Clause License +-------------------- + +(c) 2000-2013 John Lim (jlim@natsoft.com) +(c) 2014 Damien Regad, Mark Newnham and the ADOdb community +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +### DISCLAIMER + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +GNU LESSER GENERAL PUBLIC LICENSE +--------------------------------- + +_Version 2.1, February 1999_ +_Copyright © 1991, 1999 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. + +_This is the first released version of the Lesser GPL. It also counts +as the successor of the GNU Library Public License, version 2, hence +the version number 2.1._ + +### Preamble + +The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, +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 and use pieces of +it in new free programs; and that you are informed that you can do +these things. + +To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: **(1)** we copyright the +library, and **(2)** we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + +Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + +When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + +We call this license the “Lesser” General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + +For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + +Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +“work based on the library” and a “work that uses the library”. The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + +### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +**0.** This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called “this License”). +Each licensee is addressed as “you”. + +A “library” means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + +The “Library”, below, refers to any such software library or work +which has been distributed under these terms. A “work based on the +Library” means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term “modification”.) + +“Source code” for a work means the preferred form of the work for +making modifications to it. For a library, 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 library. + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + +**1.** You may copy and distribute verbatim copies of the Library's +complete 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 distribute a copy of this License along with the +Library. + +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 Library or any portion +of it, thus forming a work based on the Library, 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)** The modified work must itself be a software library. +* **b)** You must cause the files modified to carry prominent notices +stating that you changed the files and the date of any change. +* **c)** You must cause the whole of the work to be licensed at no +charge to all third parties under the terms of this License. +* **d)** If a facility in the modified Library refers to a function or a +table of data to be supplied by an application program that uses +the facility, other than as an argument passed when the facility +is invoked, then you must make a good faith effort to ensure that, +in the event an application does not supply such function or +table, the facility still operates, and performs whatever part of +its purpose remains meaningful. +(For example, a function in a library to compute square roots has +a purpose that is entirely well-defined independent of the +application. Therefore, Subsection 2d requires that any +application-supplied function or table used by this function must +be optional: if the application does not supply it, the square +root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +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 Library, 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 Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + +**3.** You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + +Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + +**4.** You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you 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. + +If distribution of 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 satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + +**5.** A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a “work that uses the Library”. Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + +However, linking a “work that uses the Library” with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a “work that uses the +library”. The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + +When a “work that uses the Library” uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + +**6.** As an exception to the Sections above, you may also combine or +link a “work that uses the Library” with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + +* **a)** Accompany the work with the complete corresponding +machine-readable source code for the Library including whatever +changes were used in the work (which must be distributed under +Sections 1 and 2 above); and, if the work is an executable linked +with the Library, with the complete machine-readable “work that +uses the Library”, as object code and/or source code, so that the +user can modify the Library and then relink to produce a modified +executable containing the modified Library. (It is understood +that the user who changes the contents of definitions files in the +Library will not necessarily be able to recompile the application +to use the modified definitions.) +* **b)** Use a suitable shared library mechanism for linking with the +Library. A suitable mechanism is one that (1) uses at run time a +copy of the library already present on the user's computer system, +rather than copying library functions into the executable, and (2) +will operate properly with a modified version of the library, if +the user installs one, as long as the modified version is +interface-compatible with the version that the work was made with. +* **c)** Accompany the work with a written offer, valid for at +least three years, to give the same user the materials +specified in Subsection 6a, above, for a charge no more +than the cost of performing this distribution. +* **d)** If distribution of the work is made by offering access to copy +from a designated place, offer equivalent access to copy the above +specified materials from the same place. +* **e)** Verify that the user has already received a copy of these +materials or that you have already sent this user a copy. + +For an executable, the required form of the “work that uses the +Library” must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be 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. + +It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + +**7.** You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + +* **a)** Accompany the combined library with a copy of the same work +based on the Library, uncombined with any other library +facilities. This must be distributed under the terms of the +Sections above. +* **b)** Give prominent notice with the combined library of the fact +that part of it is a work based on the Library, and explaining +where to find the accompanying uncombined form of the same work. + +**8.** You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library 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. + +**9.** 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 Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +**10.** Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +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 with +this License. + +**11.** 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 Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library 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 Library. + +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. + +**12.** If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library 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. + +**13.** The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +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 Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + +**14.** If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +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 + +**15.** BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY “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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +**16.** 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 LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + +_END OF TERMS AND CONDITIONS_ diff --git a/app/vendor/adodb/adodb-php/README.md b/app/vendor/adodb/adodb-php/README.md new file mode 100644 index 000000000..958cca56f --- /dev/null +++ b/app/vendor/adodb/adodb-php/README.md @@ -0,0 +1,109 @@ +ADOdb Library for PHP5 +====================== + +[![Join chat on Gitter](https://img.shields.io/gitter/room/form-data/form-data.svg)](https://gitter.im/adodb/adodb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Download ADOdb](https://img.shields.io/sourceforge/dm/adodb.svg)](https://sourceforge.net/projects/adodb/files/latest/download) + +(c) 2000-2013 John Lim (jlim@natsoft.com) +(c) 2014 Damien Regad, Mark Newnham and the ADOdb community + +Released under both [BSD 3-Clause](https://github.com/ADOdb/ADOdb/blob/master/LICENSE.md#bsd-3-clause-license) +and [GNU Lesser GPL library 2.1](https://github.com/ADOdb/ADOdb/blob/master/LICENSE.md#gnu-lesser-general-public-license) +licenses. +This means you can use it in proprietary products; +see [License](https://github.com/ADOdb/ADOdb/blob/master/LICENSE.md) for details. + +Home page: http://adodb.org/ + +> **WARNING: known issue with Associative Fetch Mode in ADOdb v5.19 +-- PLEASE UPGRADE TO v5.20 !** +> When fetching data in Associative mode (i.e. when `$ADODB_FETCH_MODE` is +> set to *ADODB_FETCH_ASSOC*), recordsets do not return any data (empty strings) +> when using some database drivers. The problem has been reported on MSSQL, +> Interbase and Foxpro, but possibly affects other drivers as well; all drivers +> derived from the above are also impacted. +> For further details, please refer to [Issue #20](https://github.com/ADOdb/ADOdb/issues/20). + + +Introduction +============ + +PHP's database access functions are not standardized. This creates a +need for a database class library to hide the differences between the +different databases (encapsulate the differences) so we can easily +switch databases. + +The library currently supports MySQL, Interbase, Sybase, PostgreSQL, Oracle, +Microsoft SQL server, Foxpro ODBC, Access ODBC, Informix, DB2, +Sybase SQL Anywhere, generic ODBC and Microsoft's ADO. + +We hope more people will contribute drivers to support other databases. + + +Installation +============ + +Unpack all the files into a directory accessible by your web server. + +To test, try modifying some of the tutorial examples. +Make sure you customize the connection settings correctly. + +You can debug using: + +``` php +debug = true; +$db->Connect($server, $user, $password, $database); +$rs = $db->Execute('select * from some_small_table'); +print "
";
+print_r($rs->GetRows());
+print "
"; +``` + + +Documentation and Examples +========================== + +Refer to the [ADOdb website](http://adodb.org/) for library documentation and examples. The documentation can also be [downloaded for offline viewing](https://sourceforge.net/projects/adodb/files/Documentation/). + +- [Main documentation](http://adodb.org/dokuwiki/doku.php?id=v5:userguide:userguide_index): Query, update and insert records using a portable API. +- [Data dictionary](http://adodb.org/dokuwiki/doku.php?id=v5:dictionary:dictionary_index) describes how to create database tables and indexes in a portable manner. +- [Database performance monitoring](http://adodb.org/dokuwiki/doku.php?id=v5:performance:performance_index) allows you to perform health checks, tune and monitor your database. +- [Database-backed sessions](http://adodb.org/dokuwiki/doku.php?id=v5:session:session_index). + +There is also a [tutorial](http://adodb.org/dokuwiki/doku.php?id=v5:userguide:mysql_tutorial) that contrasts ADOdb code with PHP native MySQL code. + + +Files +===== + +- `adodb.inc.php` is the library's main file. You only need to include this file. +- `adodb-*.inc.php` are the database specific driver code. +- `adodb-session.php` is the PHP4 session handling code. +- `test.php` contains a list of test commands to exercise the class library. +- `testdatabases.inc.php` contains the list of databases to apply the tests on. +- `Benchmark.php` is a simple benchmark to test the throughput of a SELECT +statement for databases described in testdatabases.inc.php. The benchmark +tables are created in test.php. + + +Support +======= + +To discuss with the ADOdb development team and users, connect to our +[Gitter chatroom](https://gitter.im/adodb/adodb) using your Github credentials. + +Please report bugs, issues and feature requests on Github: + +https://github.com/ADOdb/ADOdb/issues + +You may also find legacy issues in + +- the old [ADOdb forums](http://phplens.com/lens/lensforum/topics.php?id=4) on phplens.com +- the [SourceForge tickets section](http://sourceforge.net/p/adodb/_list/tickets) + +However, please note that they are not actively monitored and should +only be used as reference. diff --git a/app/vendor/adodb/adodb-php/adodb-active-record.inc.php b/app/vendor/adodb/adodb-php/adodb-active-record.inc.php new file mode 100644 index 000000000..a7f0adf7f --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-active-record.inc.php @@ -0,0 +1,1142 @@ +_dbat +$_ADODB_ACTIVE_DBS = array(); +$ACTIVE_RECORD_SAFETY = true; +$ADODB_ACTIVE_DEFVALS = false; +$ADODB_ACTIVE_CACHESECS = 0; + +class ADODB_Active_DB { + var $db; // ADOConnection + var $tables; // assoc array of ADODB_Active_Table objects, indexed by tablename +} + +class ADODB_Active_Table { + var $name; // table name + var $flds; // assoc array of adofieldobjs, indexed by fieldname + var $keys; // assoc array of primary keys, indexed by fieldname + var $_created; // only used when stored as a cached file + var $_belongsTo = array(); + var $_hasMany = array(); +} + +// $db = database connection +// $index = name of index - can be associative, for an example see +// http://phplens.com/lens/lensforum/msgs.php?id=17790 +// returns index into $_ADODB_ACTIVE_DBS +function ADODB_SetDatabaseAdapter(&$db, $index=false) +{ + global $_ADODB_ACTIVE_DBS; + + foreach($_ADODB_ACTIVE_DBS as $k => $d) { + if (PHP_VERSION >= 5) { + if ($d->db === $db) { + return $k; + } + } else { + if ($d->db->_connectionID === $db->_connectionID && $db->database == $d->db->database) { + return $k; + } + } + } + + $obj = new ADODB_Active_DB(); + $obj->db = $db; + $obj->tables = array(); + + if ($index == false) { + $index = sizeof($_ADODB_ACTIVE_DBS); + } + + $_ADODB_ACTIVE_DBS[$index] = $obj; + + return sizeof($_ADODB_ACTIVE_DBS)-1; +} + + +class ADODB_Active_Record { + static $_changeNames = true; // dynamically pluralize table names + static $_quoteNames = false; + + static $_foreignSuffix = '_id'; // + var $_dbat; // associative index pointing to ADODB_Active_DB eg. $ADODB_Active_DBS[_dbat] + var $_table; // tablename, if set in class definition then use it as table name + var $_tableat; // associative index pointing to ADODB_Active_Table, eg $ADODB_Active_DBS[_dbat]->tables[$this->_tableat] + var $_where; // where clause set in Load() + var $_saved = false; // indicates whether data is already inserted. + var $_lasterr = false; // last error message + var $_original = false; // the original values loaded or inserted, refreshed on update + + var $foreignName; // CFR: class name when in a relationship + + var $lockMode = ' for update '; // you might want to change to + + static function UseDefaultValues($bool=null) + { + global $ADODB_ACTIVE_DEFVALS; + if (isset($bool)) { + $ADODB_ACTIVE_DEFVALS = $bool; + } + return $ADODB_ACTIVE_DEFVALS; + } + + // should be static + static function SetDatabaseAdapter(&$db, $index=false) + { + return ADODB_SetDatabaseAdapter($db, $index); + } + + + public function __set($name, $value) + { + $name = str_replace(' ', '_', $name); + $this->$name = $value; + } + + // php5 constructor + function __construct($table = false, $pkeyarr=false, $db=false) + { + global $ADODB_ASSOC_CASE,$_ADODB_ACTIVE_DBS; + + if ($db == false && is_object($pkeyarr)) { + $db = $pkeyarr; + $pkeyarr = false; + } + + if (!$table) { + if (!empty($this->_table)) { + $table = $this->_table; + } + else $table = $this->_pluralize(get_class($this)); + } + $this->foreignName = strtolower(get_class($this)); // CFR: default foreign name + if ($db) { + $this->_dbat = ADODB_Active_Record::SetDatabaseAdapter($db); + } else if (!isset($this->_dbat)) { + if (sizeof($_ADODB_ACTIVE_DBS) == 0) { + $this->Error( + "No database connection set; use ADOdb_Active_Record::SetDatabaseAdapter(\$db)", + 'ADODB_Active_Record::__constructor' + ); + } + end($_ADODB_ACTIVE_DBS); + $this->_dbat = key($_ADODB_ACTIVE_DBS); + } + + $this->_table = $table; + $this->_tableat = $table; # reserved for setting the assoc value to a non-table name, eg. the sql string in future + + $this->UpdateActiveTable($pkeyarr); + } + + function __wakeup() + { + $class = get_class($this); + new $class; + } + + function _pluralize($table) + { + if (!ADODB_Active_Record::$_changeNames) { + return $table; + } + + $ut = strtoupper($table); + $len = strlen($table); + $lastc = $ut[$len-1]; + $lastc2 = substr($ut,$len-2); + switch ($lastc) { + case 'S': + return $table.'es'; + case 'Y': + return substr($table,0,$len-1).'ies'; + case 'X': + return $table.'es'; + case 'H': + if ($lastc2 == 'CH' || $lastc2 == 'SH') { + return $table.'es'; + } + default: + return $table.'s'; + } + } + + // CFR Lamest singular inflector ever - @todo Make it real! + // Note: There is an assumption here...and it is that the argument's length >= 4 + function _singularize($tables) + { + + if (!ADODB_Active_Record::$_changeNames) { + return $table; + } + + $ut = strtoupper($tables); + $len = strlen($tables); + if($ut[$len-1] != 'S') { + return $tables; // I know...forget oxen + } + if($ut[$len-2] != 'E') { + return substr($tables, 0, $len-1); + } + switch($ut[$len-3]) { + case 'S': + case 'X': + return substr($tables, 0, $len-2); + case 'I': + return substr($tables, 0, $len-3) . 'y'; + case 'H'; + if($ut[$len-4] == 'C' || $ut[$len-4] == 'S') { + return substr($tables, 0, $len-2); + } + default: + return substr($tables, 0, $len-1); // ? + } + } + + function hasMany($foreignRef, $foreignKey = false, $foreignClass = 'ADODB_Active_Record') + { + $ar = new $foreignClass($foreignRef); + $ar->foreignName = $foreignRef; + $ar->UpdateActiveTable(); + $ar->foreignKey = ($foreignKey) ? $foreignKey : $foreignRef.ADODB_Active_Record::$_foreignSuffix; + $table =& $this->TableInfo(); + $table->_hasMany[$foreignRef] = $ar; + # $this->$foreignRef = $this->_hasMany[$foreignRef]; // WATCHME Removed assignment by ref. to please __get() + } + + // use when you don't want ADOdb to auto-pluralize tablename + static function TableHasMany($table, $foreignRef, $foreignKey = false, $foreignClass = 'ADODB_Active_Record') + { + $ar = new ADODB_Active_Record($table); + $ar->hasMany($foreignRef, $foreignKey, $foreignClass); + } + + // use when you don't want ADOdb to auto-pluralize tablename + static function TableKeyHasMany($table, $tablePKey, $foreignRef, $foreignKey = false, $foreignClass = 'ADODB_Active_Record') + { + if (!is_array($tablePKey)) { + $tablePKey = array($tablePKey); + } + $ar = new ADODB_Active_Record($table,$tablePKey); + $ar->hasMany($foreignRef, $foreignKey, $foreignClass); + } + + + // use when you want ADOdb to auto-pluralize tablename for you. Note that the class must already be defined. + // e.g. class Person will generate relationship for table Persons + static function ClassHasMany($parentclass, $foreignRef, $foreignKey = false, $foreignClass = 'ADODB_Active_Record') + { + $ar = new $parentclass(); + $ar->hasMany($foreignRef, $foreignKey, $foreignClass); + } + + + function belongsTo($foreignRef,$foreignKey=false, $parentKey='', $parentClass = 'ADODB_Active_Record') + { + global $inflector; + + $ar = new $parentClass($this->_pluralize($foreignRef)); + $ar->foreignName = $foreignRef; + $ar->parentKey = $parentKey; + $ar->UpdateActiveTable(); + $ar->foreignKey = ($foreignKey) ? $foreignKey : $foreignRef.ADODB_Active_Record::$_foreignSuffix; + + $table =& $this->TableInfo(); + $table->_belongsTo[$foreignRef] = $ar; + # $this->$foreignRef = $this->_belongsTo[$foreignRef]; + } + + static function ClassBelongsTo($class, $foreignRef, $foreignKey=false, $parentKey='', $parentClass = 'ADODB_Active_Record') + { + $ar = new $class(); + $ar->belongsTo($foreignRef, $foreignKey, $parentKey, $parentClass); + } + + static function TableBelongsTo($table, $foreignRef, $foreignKey=false, $parentKey='', $parentClass = 'ADODB_Active_Record') + { + $ar = new ADOdb_Active_Record($table); + $ar->belongsTo($foreignRef, $foreignKey, $parentKey, $parentClass); + } + + static function TableKeyBelongsTo($table, $tablePKey, $foreignRef, $foreignKey=false, $parentKey='', $parentClass = 'ADODB_Active_Record') + { + if (!is_array($tablePKey)) { + $tablePKey = array($tablePKey); + } + $ar = new ADOdb_Active_Record($table, $tablePKey); + $ar->belongsTo($foreignRef, $foreignKey, $parentKey, $parentClass); + } + + + /** + * __get Access properties - used for lazy loading + * + * @param mixed $name + * @access protected + * @return mixed + */ + function __get($name) + { + return $this->LoadRelations($name, '', -1, -1); + } + + /** + * @param string $name + * @param string $whereOrderBy : eg. ' AND field1 = value ORDER BY field2' + * @param offset + * @param limit + * @return mixed + */ + function LoadRelations($name, $whereOrderBy='', $offset=-1,$limit=-1) + { + $extras = array(); + $table = $this->TableInfo(); + if ($limit >= 0) { + $extras['limit'] = $limit; + } + if ($offset >= 0) { + $extras['offset'] = $offset; + } + + if (strlen($whereOrderBy)) { + if (!preg_match('/^[ \n\r]*AND/i', $whereOrderBy)) { + if (!preg_match('/^[ \n\r]*ORDER[ \n\r]/i', $whereOrderBy)) { + $whereOrderBy = 'AND ' . $whereOrderBy; + } + } + } + + if(!empty($table->_belongsTo[$name])) { + $obj = $table->_belongsTo[$name]; + $columnName = $obj->foreignKey; + if(empty($this->$columnName)) { + $this->$name = null; + } + else { + if ($obj->parentKey) { + $key = $obj->parentKey; + } + else { + $key = reset($table->keys); + } + + $arrayOfOne = $obj->Find($key.'='.$this->$columnName.' '.$whereOrderBy,false,false,$extras); + if ($arrayOfOne) { + $this->$name = $arrayOfOne[0]; + return $arrayOfOne[0]; + } + } + } + if(!empty($table->_hasMany[$name])) { + $obj = $table->_hasMany[$name]; + $key = reset($table->keys); + $id = @$this->$key; + if (!is_numeric($id)) { + $db = $this->DB(); + $id = $db->qstr($id); + } + $objs = $obj->Find($obj->foreignKey.'='.$id. ' '.$whereOrderBy,false,false,$extras); + if (!$objs) { + $objs = array(); + } + $this->$name = $objs; + return $objs; + } + + return array(); + } + ////////////////////////////////// + + // update metadata + function UpdateActiveTable($pkeys=false,$forceUpdate=false) + { + global $ADODB_ASSOC_CASE,$_ADODB_ACTIVE_DBS , $ADODB_CACHE_DIR, $ADODB_ACTIVE_CACHESECS; + global $ADODB_ACTIVE_DEFVALS,$ADODB_FETCH_MODE; + + $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; + + $table = $this->_table; + $tables = $activedb->tables; + $tableat = $this->_tableat; + if (!$forceUpdate && !empty($tables[$tableat])) { + + $acttab = $tables[$tableat]; + foreach($acttab->flds as $name => $fld) { + if ($ADODB_ACTIVE_DEFVALS && isset($fld->default_value)) { + $this->$name = $fld->default_value; + } + else { + $this->$name = null; + } + } + return; + } + $db = $activedb->db; + $fname = $ADODB_CACHE_DIR . '/adodb_' . $db->databaseType . '_active_'. $table . '.cache'; + if (!$forceUpdate && $ADODB_ACTIVE_CACHESECS && $ADODB_CACHE_DIR && file_exists($fname)) { + $fp = fopen($fname,'r'); + @flock($fp, LOCK_SH); + $acttab = unserialize(fread($fp,100000)); + fclose($fp); + if ($acttab->_created + $ADODB_ACTIVE_CACHESECS - (abs(rand()) % 16) > time()) { + // abs(rand()) randomizes deletion, reducing contention to delete/refresh file + // ideally, you should cache at least 32 secs + + foreach($acttab->flds as $name => $fld) { + if ($ADODB_ACTIVE_DEFVALS && isset($fld->default_value)) { + $this->$name = $fld->default_value; + } + else { + $this->$name = null; + } + } + + $activedb->tables[$table] = $acttab; + + //if ($db->debug) ADOConnection::outp("Reading cached active record file: $fname"); + return; + } else if ($db->debug) { + ADOConnection::outp("Refreshing cached active record file: $fname"); + } + } + $activetab = new ADODB_Active_Table(); + $activetab->name = $table; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + if ($db->fetchMode !== false) { + $savem = $db->SetFetchMode(false); + } + + $cols = $db->MetaColumns($table); + + if (isset($savem)) { + $db->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + if (!$cols) { + $this->Error("Invalid table name: $table",'UpdateActiveTable'); + return false; + } + $fld = reset($cols); + if (!$pkeys) { + if (isset($fld->primary_key)) { + $pkeys = array(); + foreach($cols as $name => $fld) { + if (!empty($fld->primary_key)) { + $pkeys[] = $name; + } + } + } else + $pkeys = $this->GetPrimaryKeys($db, $table); + } + if (empty($pkeys)) { + $this->Error("No primary key found for table $table",'UpdateActiveTable'); + return false; + } + + $attr = array(); + $keys = array(); + + switch($ADODB_ASSOC_CASE) { + case 0: + foreach($cols as $name => $fldobj) { + $name = strtolower($name); + if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) { + $this->$name = $fldobj->default_value; + } + else { + $this->$name = null; + } + $attr[$name] = $fldobj; + } + foreach($pkeys as $k => $name) { + $keys[strtolower($name)] = strtolower($name); + } + break; + + case 1: + foreach($cols as $name => $fldobj) { + $name = strtoupper($name); + + if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) { + $this->$name = $fldobj->default_value; + } + else { + $this->$name = null; + } + $attr[$name] = $fldobj; + } + + foreach($pkeys as $k => $name) { + $keys[strtoupper($name)] = strtoupper($name); + } + break; + default: + foreach($cols as $name => $fldobj) { + $name = ($fldobj->name); + + if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) { + $this->$name = $fldobj->default_value; + } + else { + $this->$name = null; + } + $attr[$name] = $fldobj; + } + foreach($pkeys as $k => $name) { + $keys[$name] = $cols[$name]->name; + } + break; + } + + $activetab->keys = $keys; + $activetab->flds = $attr; + + if ($ADODB_ACTIVE_CACHESECS && $ADODB_CACHE_DIR) { + $activetab->_created = time(); + $s = serialize($activetab); + if (!function_exists('adodb_write_file')) { + include(ADODB_DIR.'/adodb-csvlib.inc.php'); + } + adodb_write_file($fname,$s); + } + if (isset($activedb->tables[$table])) { + $oldtab = $activedb->tables[$table]; + + if ($oldtab) { + $activetab->_belongsTo = $oldtab->_belongsTo; + $activetab->_hasMany = $oldtab->_hasMany; + } + } + $activedb->tables[$table] = $activetab; + } + + function GetPrimaryKeys(&$db, $table) + { + return $db->MetaPrimaryKeys($table); + } + + // error handler for both PHP4+5. + function Error($err,$fn) + { + global $_ADODB_ACTIVE_DBS; + + $fn = get_class($this).'::'.$fn; + $this->_lasterr = $fn.': '.$err; + + if ($this->_dbat < 0) { + $db = false; + } + else { + $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; + $db = $activedb->db; + } + + if (function_exists('adodb_throw')) { + if (!$db) { + adodb_throw('ADOdb_Active_Record', $fn, -1, $err, 0, 0, false); + } + else { + adodb_throw($db->databaseType, $fn, -1, $err, 0, 0, $db); + } + } else { + if (!$db || $db->debug) { + ADOConnection::outp($this->_lasterr); + } + } + + } + + // return last error message + function ErrorMsg() + { + if (!function_exists('adodb_throw')) { + if ($this->_dbat < 0) { + $db = false; + } + else { + $db = $this->DB(); + } + + // last error could be database error too + if ($db && $db->ErrorMsg()) { + return $db->ErrorMsg(); + } + } + return $this->_lasterr; + } + + function ErrorNo() + { + if ($this->_dbat < 0) { + return -9999; // no database connection... + } + $db = $this->DB(); + + return (int) $db->ErrorNo(); + } + + + // retrieve ADOConnection from _ADODB_Active_DBs + function DB() + { + global $_ADODB_ACTIVE_DBS; + + if ($this->_dbat < 0) { + $false = false; + $this->Error("No database connection set: use ADOdb_Active_Record::SetDatabaseAdaptor(\$db)", "DB"); + return $false; + } + $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; + $db = $activedb->db; + return $db; + } + + // retrieve ADODB_Active_Table + function &TableInfo() + { + global $_ADODB_ACTIVE_DBS; + $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; + $table = $activedb->tables[$this->_tableat]; + return $table; + } + + + // I have an ON INSERT trigger on a table that sets other columns in the table. + // So, I find that for myTable, I want to reload an active record after saving it. -- Malcolm Cook + function Reload() + { + $db = $this->DB(); + if (!$db) { + return false; + } + $table = $this->TableInfo(); + $where = $this->GenWhere($db, $table); + return($this->Load($where)); + } + + + // set a numeric array (using natural table field ordering) as object properties + function Set(&$row) + { + global $ACTIVE_RECORD_SAFETY; + + $db = $this->DB(); + + if (!$row) { + $this->_saved = false; + return false; + } + + $this->_saved = true; + + $table = $this->TableInfo(); + if ($ACTIVE_RECORD_SAFETY && sizeof($table->flds) != sizeof($row)) { + # + $bad_size = TRUE; + if (sizeof($row) == 2 * sizeof($table->flds)) { + // Only keep string keys + $keys = array_filter(array_keys($row), 'is_string'); + if (sizeof($keys) == sizeof($table->flds)) { + $bad_size = FALSE; + } + } + if ($bad_size) { + $this->Error("Table structure of $this->_table has changed","Load"); + return false; + } + # + } + else + $keys = array_keys($row); + + # + reset($keys); + $this->_original = array(); + foreach($table->flds as $name=>$fld) { + $value = $row[current($keys)]; + $this->$name = $value; + $this->_original[] = $value; + next($keys); + } + + # + return true; + } + + // get last inserted id for INSERT + function LastInsertID(&$db,$fieldname) + { + if ($db->hasInsertID) { + $val = $db->Insert_ID($this->_table,$fieldname); + } + else { + $val = false; + } + + if (is_null($val) || $val === false) { + // this might not work reliably in multi-user environment + return $db->GetOne("select max(".$fieldname.") from ".$this->_table); + } + return $val; + } + + // quote data in where clause + function doquote(&$db, $val,$t) + { + switch($t) { + case 'L': + if (strpos($db->databaseType,'postgres') !== false) { + return $db->qstr($val); + } + case 'D': + case 'T': + if (empty($val)) { + return 'null'; + } + case 'B': + case 'N': + case 'C': + case 'X': + if (is_null($val)) { + return 'null'; + } + + if (strlen($val)>0 && + (strncmp($val,"'",1) != 0 || substr($val,strlen($val)-1,1) != "'") + ) { + return $db->qstr($val); + break; + } + default: + return $val; + break; + } + } + + // generate where clause for an UPDATE/SELECT + function GenWhere(&$db, &$table) + { + $keys = $table->keys; + $parr = array(); + + foreach($keys as $k) { + $f = $table->flds[$k]; + if ($f) { + $parr[] = $k.' = '.$this->doquote($db,$this->$k,$db->MetaType($f->type)); + } + } + return implode(' and ', $parr); + } + + + function _QName($n,$db=false) + { + if (!ADODB_Active_Record::$_quoteNames) { + return $n; + } + if (!$db) { + $db = $this->DB(); + if (!$db) { + return false; + } + } + return $db->nameQuote.$n.$db->nameQuote; + } + + //------------------------------------------------------------ Public functions below + + function Load($where=null,$bindarr=false, $lock = false) + { + global $ADODB_FETCH_MODE; + + $db = $this->DB(); + if (!$db) { + return false; + } + $this->_where = $where; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($db->fetchMode !== false) { + $savem = $db->SetFetchMode(false); + } + + $qry = "select * from ".$this->_table; + + if($where) { + $qry .= ' WHERE '.$where; + } + if ($lock) { + $qry .= $this->lockMode; + } + + $row = $db->GetRow($qry,$bindarr); + + if (isset($savem)) { + $db->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + return $this->Set($row); + } + + function LoadLocked($where=null, $bindarr=false) + { + $this->Load($where,$bindarr,true); + } + + # useful for multiple record inserts + # see http://phplens.com/lens/lensforum/msgs.php?id=17795 + function Reset() + { + $this->_where=null; + $this->_saved = false; + $this->_lasterr = false; + $this->_original = false; + $vars=get_object_vars($this); + foreach($vars as $k=>$v){ + if(substr($k,0,1)!=='_'){ + $this->{$k}=null; + } + } + $this->foreignName=strtolower(get_class($this)); + return true; + } + + // false on error + function Save() + { + if ($this->_saved) { + $ok = $this->Update(); + } + else { + $ok = $this->Insert(); + } + + return $ok; + } + + + // false on error + function Insert() + { + $db = $this->DB(); + if (!$db) { + return false; + } + $cnt = 0; + $table = $this->TableInfo(); + + $valarr = array(); + $names = array(); + $valstr = array(); + + foreach($table->flds as $name=>$fld) { + $val = $this->$name; + if(!is_array($val) || !is_null($val) || !array_key_exists($name, $table->keys)) { + $valarr[] = $val; + $names[] = $this->_QName($name,$db); + $valstr[] = $db->Param($cnt); + $cnt += 1; + } + } + + if (empty($names)){ + foreach($table->flds as $name=>$fld) { + $valarr[] = null; + $names[] = $name; + $valstr[] = $db->Param($cnt); + $cnt += 1; + } + } + $sql = 'INSERT INTO '.$this->_table."(".implode(',',$names).') VALUES ('.implode(',',$valstr).')'; + $ok = $db->Execute($sql,$valarr); + + if ($ok) { + $this->_saved = true; + $autoinc = false; + foreach($table->keys as $k) { + if (is_null($this->$k)) { + $autoinc = true; + break; + } + } + if ($autoinc && sizeof($table->keys) == 1) { + $k = reset($table->keys); + $this->$k = $this->LastInsertID($db,$k); + } + } + + $this->_original = $valarr; + return !empty($ok); + } + + function Delete() + { + $db = $this->DB(); + if (!$db) { + return false; + } + $table = $this->TableInfo(); + + $where = $this->GenWhere($db,$table); + $sql = 'DELETE FROM '.$this->_table.' WHERE '.$where; + $ok = $db->Execute($sql); + + return $ok ? true : false; + } + + // returns an array of active record objects + function Find($whereOrderBy,$bindarr=false,$pkeysArr=false,$extra=array()) + { + $db = $this->DB(); + if (!$db || empty($this->_table)) { + return false; + } + $arr = $db->GetActiveRecordsClass(get_class($this),$this->_table, $whereOrderBy,$bindarr,$pkeysArr,$extra); + return $arr; + } + + // returns 0 on error, 1 on update, 2 on insert + function Replace() + { + global $ADODB_ASSOC_CASE; + + $db = $this->DB(); + if (!$db) { + return false; + } + $table = $this->TableInfo(); + + $pkey = $table->keys; + + foreach($table->flds as $name=>$fld) { + $val = $this->$name; + /* + if (is_null($val)) { + if (isset($fld->not_null) && $fld->not_null) { + if (isset($fld->default_value) && strlen($fld->default_value)) { + continue; + } + else { + $this->Error("Cannot update null into $name","Replace"); + return false; + } + } + }*/ + if (is_null($val) && !empty($fld->auto_increment)) { + continue; + } + + if (is_array($val)) { + continue; + } + + $t = $db->MetaType($fld->type); + $arr[$name] = $this->doquote($db,$val,$t); + $valarr[] = $val; + } + + if (!is_array($pkey)) { + $pkey = array($pkey); + } + + if ($ADODB_ASSOC_CASE == 0) { + foreach($pkey as $k => $v) + $pkey[$k] = strtolower($v); + } + elseif ($ADODB_ASSOC_CASE == 1) { + foreach($pkey as $k => $v) { + $pkey[$k] = strtoupper($v); + } + } + + $ok = $db->Replace($this->_table,$arr,$pkey); + if ($ok) { + $this->_saved = true; // 1= update 2=insert + if ($ok == 2) { + $autoinc = false; + foreach($table->keys as $k) { + if (is_null($this->$k)) { + $autoinc = true; + break; + } + } + if ($autoinc && sizeof($table->keys) == 1) { + $k = reset($table->keys); + $this->$k = $this->LastInsertID($db,$k); + } + } + + $this->_original = $valarr; + } + return $ok; + } + + // returns 0 on error, 1 on update, -1 if no change in data (no update) + function Update() + { + $db = $this->DB(); + if (!$db) { + return false; + } + $table = $this->TableInfo(); + + $where = $this->GenWhere($db, $table); + + if (!$where) { + $this->error("Where missing for table $table", "Update"); + return false; + } + $valarr = array(); + $neworig = array(); + $pairs = array(); + $i = -1; + $cnt = 0; + foreach($table->flds as $name=>$fld) { + $i += 1; + $val = $this->$name; + $neworig[] = $val; + + if (isset($table->keys[$name]) || is_array($val)) { + continue; + } + + if (is_null($val)) { + if (isset($fld->not_null) && $fld->not_null) { + if (isset($fld->default_value) && strlen($fld->default_value)) { + continue; + } + else { + $this->Error("Cannot set field $name to NULL","Update"); + return false; + } + } + } + + if (isset($this->_original[$i]) && strcmp($val,$this->_original[$i]) == 0) { + continue; + } + + if (is_null($this->_original[$i]) && is_null($val)) { + continue; + } + + $valarr[] = $val; + $pairs[] = $this->_QName($name,$db).'='.$db->Param($cnt); + $cnt += 1; + } + + + if (!$cnt) { + return -1; + } + + $sql = 'UPDATE '.$this->_table." SET ".implode(",",$pairs)." WHERE ".$where; + $ok = $db->Execute($sql,$valarr); + if ($ok) { + $this->_original = $neworig; + return 1; + } + return 0; + } + + function GetAttributeNames() + { + $table = $this->TableInfo(); + if (!$table) { + return false; + } + return array_keys($table->flds); + } + +}; + +function adodb_GetActiveRecordsClass(&$db, $class, $table,$whereOrderBy,$bindarr, $primkeyArr, + $extra) +{ +global $_ADODB_ACTIVE_DBS; + + + $save = $db->SetFetchMode(ADODB_FETCH_NUM); + $qry = "select * from ".$table; + + if (!empty($whereOrderBy)) { + $qry .= ' WHERE '.$whereOrderBy; + } + if(isset($extra['limit'])) { + $rows = false; + if(isset($extra['offset'])) { + $rs = $db->SelectLimit($qry, $extra['limit'], $extra['offset'],$bindarr); + } else { + $rs = $db->SelectLimit($qry, $extra['limit'],-1,$bindarr); + } + if ($rs) { + while (!$rs->EOF) { + $rows[] = $rs->fields; + $rs->MoveNext(); + } + } + } else + $rows = $db->GetAll($qry,$bindarr); + + $db->SetFetchMode($save); + + $false = false; + + if ($rows === false) { + return $false; + } + + + if (!class_exists($class)) { + $db->outp_throw("Unknown class $class in GetActiveRecordsClass()",'GetActiveRecordsClass'); + return $false; + } + $arr = array(); + // arrRef will be the structure that knows about our objects. + // It is an associative array. + // We will, however, return arr, preserving regular 0.. order so that + // obj[0] can be used by app developpers. + $arrRef = array(); + $bTos = array(); // Will store belongTo's indices if any + foreach($rows as $row) { + + $obj = new $class($table,$primkeyArr,$db); + if ($obj->ErrorNo()){ + $db->_errorMsg = $obj->ErrorMsg(); + return $false; + } + $obj->Set($row); + $arr[] = $obj; + } // foreach($rows as $row) + + return $arr; +} diff --git a/app/vendor/adodb/adodb-php/adodb-active-recordx.inc.php b/app/vendor/adodb/adodb-php/adodb-active-recordx.inc.php new file mode 100644 index 000000000..7f553f1e0 --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-active-recordx.inc.php @@ -0,0 +1,1497 @@ +_dbat +$_ADODB_ACTIVE_DBS = array(); +$ACTIVE_RECORD_SAFETY = true; // CFR: disabled while playing with relations +$ADODB_ACTIVE_DEFVALS = false; + +class ADODB_Active_DB { + var $db; // ADOConnection + var $tables; // assoc array of ADODB_Active_Table objects, indexed by tablename +} + +class ADODB_Active_Table { + var $name; // table name + var $flds; // assoc array of adofieldobjs, indexed by fieldname + var $keys; // assoc array of primary keys, indexed by fieldname + var $_created; // only used when stored as a cached file + var $_belongsTo = array(); + var $_hasMany = array(); + var $_colsCount; // total columns count, including relations + + function updateColsCount() + { + $this->_colsCount = sizeof($this->flds); + foreach($this->_belongsTo as $foreignTable) + $this->_colsCount += sizeof($foreignTable->TableInfo()->flds); + foreach($this->_hasMany as $foreignTable) + $this->_colsCount += sizeof($foreignTable->TableInfo()->flds); + } +} + +// returns index into $_ADODB_ACTIVE_DBS +function ADODB_SetDatabaseAdapter(&$db) +{ + global $_ADODB_ACTIVE_DBS; + + foreach($_ADODB_ACTIVE_DBS as $k => $d) { + if (PHP_VERSION >= 5) { + if ($d->db === $db) { + return $k; + } + } else { + if ($d->db->_connectionID === $db->_connectionID && $db->database == $d->db->database) { + return $k; + } + } + } + + $obj = new ADODB_Active_DB(); + $obj->db = $db; + $obj->tables = array(); + + $_ADODB_ACTIVE_DBS[] = $obj; + + return sizeof($_ADODB_ACTIVE_DBS)-1; +} + + +class ADODB_Active_Record { + static $_changeNames = true; // dynamically pluralize table names + static $_foreignSuffix = '_id'; // + var $_dbat; // associative index pointing to ADODB_Active_DB eg. $ADODB_Active_DBS[_dbat] + var $_table; // tablename, if set in class definition then use it as table name + var $_sTable; // singularized table name + var $_pTable; // pluralized table name + var $_tableat; // associative index pointing to ADODB_Active_Table, eg $ADODB_Active_DBS[_dbat]->tables[$this->_tableat] + var $_where; // where clause set in Load() + var $_saved = false; // indicates whether data is already inserted. + var $_lasterr = false; // last error message + var $_original = false; // the original values loaded or inserted, refreshed on update + + var $foreignName; // CFR: class name when in a relationship + + static function UseDefaultValues($bool=null) + { + global $ADODB_ACTIVE_DEFVALS; + if (isset($bool)) { + $ADODB_ACTIVE_DEFVALS = $bool; + } + return $ADODB_ACTIVE_DEFVALS; + } + + // should be static + static function SetDatabaseAdapter(&$db) + { + return ADODB_SetDatabaseAdapter($db); + } + + + public function __set($name, $value) + { + $name = str_replace(' ', '_', $name); + $this->$name = $value; + } + + // php5 constructor + // Note: if $table is defined, then we will use it as our table name + // Otherwise we will use our classname... + // In our database, table names are pluralized (because there can be + // more than one row!) + // Similarly, if $table is defined here, it has to be plural form. + // + // $options is an array that allows us to tweak the constructor's behaviour + // if $options['refresh'] is true, we re-scan our metadata information + // if $options['new'] is true, we forget all relations + function __construct($table = false, $pkeyarr=false, $db=false, $options=array()) + { + global $ADODB_ASSOC_CASE,$_ADODB_ACTIVE_DBS; + + if ($db == false && is_object($pkeyarr)) { + $db = $pkeyarr; + $pkeyarr = false; + } + + if($table) { + // table argument exists. It is expected to be + // already plural form. + $this->_pTable = $table; + $this->_sTable = $this->_singularize($this->_pTable); + } + else { + // We will use current classname as table name. + // We need to pluralize it for the real table name. + $this->_sTable = strtolower(get_class($this)); + $this->_pTable = $this->_pluralize($this->_sTable); + } + $this->_table = &$this->_pTable; + + $this->foreignName = $this->_sTable; // CFR: default foreign name (singular) + + if ($db) { + $this->_dbat = ADODB_Active_Record::SetDatabaseAdapter($db); + } else + $this->_dbat = sizeof($_ADODB_ACTIVE_DBS)-1; + + + if ($this->_dbat < 0) { + $this->Error( + "No database connection set; use ADOdb_Active_Record::SetDatabaseAdapter(\$db)", + 'ADODB_Active_Record::__constructor' + ); + } + + $this->_tableat = $this->_table; # reserved for setting the assoc value to a non-table name, eg. the sql string in future + + // CFR: Just added this option because UpdateActiveTable() can refresh its information + // but there was no way to ask it to do that. + $forceUpdate = (isset($options['refresh']) && true === $options['refresh']); + $this->UpdateActiveTable($pkeyarr, $forceUpdate); + if(isset($options['new']) && true === $options['new']) { + $table =& $this->TableInfo(); + unset($table->_hasMany); + unset($table->_belongsTo); + $table->_hasMany = array(); + $table->_belongsTo = array(); + } + } + + function __wakeup() + { + $class = get_class($this); + new $class; + } + + // CFR: Constants found in Rails + static $IrregularP = array( + 'PERSON' => 'people', + 'MAN' => 'men', + 'WOMAN' => 'women', + 'CHILD' => 'children', + 'COW' => 'kine', + ); + + static $IrregularS = array( + 'PEOPLE' => 'PERSON', + 'MEN' => 'man', + 'WOMEN' => 'woman', + 'CHILDREN' => 'child', + 'KINE' => 'cow', + ); + + static $WeIsI = array( + 'EQUIPMENT' => true, + 'INFORMATION' => true, + 'RICE' => true, + 'MONEY' => true, + 'SPECIES' => true, + 'SERIES' => true, + 'FISH' => true, + 'SHEEP' => true, + ); + + function _pluralize($table) + { + if (!ADODB_Active_Record::$_changeNames) { + return $table; + } + $ut = strtoupper($table); + if(isset(self::$WeIsI[$ut])) { + return $table; + } + if(isset(self::$IrregularP[$ut])) { + return self::$IrregularP[$ut]; + } + $len = strlen($table); + $lastc = $ut[$len-1]; + $lastc2 = substr($ut,$len-2); + switch ($lastc) { + case 'S': + return $table.'es'; + case 'Y': + return substr($table,0,$len-1).'ies'; + case 'X': + return $table.'es'; + case 'H': + if ($lastc2 == 'CH' || $lastc2 == 'SH') { + return $table.'es'; + } + default: + return $table.'s'; + } + } + + // CFR Lamest singular inflector ever - @todo Make it real! + // Note: There is an assumption here...and it is that the argument's length >= 4 + function _singularize($table) + { + + if (!ADODB_Active_Record::$_changeNames) { + return $table; + } + $ut = strtoupper($table); + if(isset(self::$WeIsI[$ut])) { + return $table; + } + if(isset(self::$IrregularS[$ut])) { + return self::$IrregularS[$ut]; + } + $len = strlen($table); + if($ut[$len-1] != 'S') { + return $table; // I know...forget oxen + } + if($ut[$len-2] != 'E') { + return substr($table, 0, $len-1); + } + switch($ut[$len-3]) { + case 'S': + case 'X': + return substr($table, 0, $len-2); + case 'I': + return substr($table, 0, $len-3) . 'y'; + case 'H'; + if($ut[$len-4] == 'C' || $ut[$len-4] == 'S') { + return substr($table, 0, $len-2); + } + default: + return substr($table, 0, $len-1); // ? + } + } + + /* + * ar->foreignName will contain the name of the tables associated with this table because + * these other tables' rows may also be referenced by this table using theirname_id or the provided + * foreign keys (this index name is stored in ar->foreignKey) + * + * this-table.id = other-table-#1.this-table_id + * = other-table-#2.this-table_id + */ + function hasMany($foreignRef,$foreignKey=false) + { + $ar = new ADODB_Active_Record($foreignRef); + $ar->foreignName = $foreignRef; + $ar->UpdateActiveTable(); + $ar->foreignKey = ($foreignKey) ? $foreignKey : strtolower(get_class($this)) . self::$_foreignSuffix; + + $table =& $this->TableInfo(); + if(!isset($table->_hasMany[$foreignRef])) { + $table->_hasMany[$foreignRef] = $ar; + $table->updateColsCount(); + } +# @todo Can I make this guy be lazy? + $this->$foreignRef = $table->_hasMany[$foreignRef]; // WATCHME Removed assignment by ref. to please __get() + } + + /** + * ar->foreignName will contain the name of the tables associated with this table because + * this table's rows may also be referenced by those tables using thistable_id or the provided + * foreign keys (this index name is stored in ar->foreignKey) + * + * this-table.other-table_id = other-table.id + */ + function belongsTo($foreignRef,$foreignKey=false) + { + global $inflector; + + $ar = new ADODB_Active_Record($this->_pluralize($foreignRef)); + $ar->foreignName = $foreignRef; + $ar->UpdateActiveTable(); + $ar->foreignKey = ($foreignKey) ? $foreignKey : $ar->foreignName . self::$_foreignSuffix; + + $table =& $this->TableInfo(); + if(!isset($table->_belongsTo[$foreignRef])) { + $table->_belongsTo[$foreignRef] = $ar; + $table->updateColsCount(); + } + $this->$foreignRef = $table->_belongsTo[$foreignRef]; + } + + /** + * __get Access properties - used for lazy loading + * + * @param mixed $name + * @access protected + * @return void + */ + function __get($name) + { + return $this->LoadRelations($name, '', -1. -1); + } + + function LoadRelations($name, $whereOrderBy, $offset=-1, $limit=-1) + { + $extras = array(); + if($offset >= 0) { + $extras['offset'] = $offset; + } + if($limit >= 0) { + $extras['limit'] = $limit; + } + $table =& $this->TableInfo(); + + if (strlen($whereOrderBy)) { + if (!preg_match('/^[ \n\r]*AND/i',$whereOrderBy)) { + if (!preg_match('/^[ \n\r]*ORDER[ \n\r]/i',$whereOrderBy)) { + $whereOrderBy = 'AND '.$whereOrderBy; + } + } + } + + if(!empty($table->_belongsTo[$name])) { + $obj = $table->_belongsTo[$name]; + $columnName = $obj->foreignKey; + if(empty($this->$columnName)) { + $this->$name = null; + } + else { + if(($k = reset($obj->TableInfo()->keys))) { + $belongsToId = $k; + } + else { + $belongsToId = 'id'; + } + + $arrayOfOne = + $obj->Find( + $belongsToId.'='.$this->$columnName.' '.$whereOrderBy, false, false, $extras); + $this->$name = $arrayOfOne[0]; + } + return $this->$name; + } + if(!empty($table->_hasMany[$name])) { + $obj = $table->_hasMany[$name]; + if(($k = reset($table->keys))) { + $hasManyId = $k; + } + else { + $hasManyId = 'id'; + } + + $this->$name = + $obj->Find( + $obj->foreignKey.'='.$this->$hasManyId.' '.$whereOrderBy, false, false, $extras); + return $this->$name; + } + } + ////////////////////////////////// + + // update metadata + function UpdateActiveTable($pkeys=false,$forceUpdate=false) + { + global $ADODB_ASSOC_CASE,$_ADODB_ACTIVE_DBS , $ADODB_CACHE_DIR, $ADODB_ACTIVE_CACHESECS; + global $ADODB_ACTIVE_DEFVALS, $ADODB_FETCH_MODE; + + $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; + + $table = $this->_table; + $tables = $activedb->tables; + $tableat = $this->_tableat; + if (!$forceUpdate && !empty($tables[$tableat])) { + + $tobj = $tables[$tableat]; + foreach($tobj->flds as $name => $fld) { + if ($ADODB_ACTIVE_DEFVALS && isset($fld->default_value)) { + $this->$name = $fld->default_value; + } + else { + $this->$name = null; + } + } + return; + } + + $db = $activedb->db; + $fname = $ADODB_CACHE_DIR . '/adodb_' . $db->databaseType . '_active_'. $table . '.cache'; + if (!$forceUpdate && $ADODB_ACTIVE_CACHESECS && $ADODB_CACHE_DIR && file_exists($fname)) { + $fp = fopen($fname,'r'); + @flock($fp, LOCK_SH); + $acttab = unserialize(fread($fp,100000)); + fclose($fp); + if ($acttab->_created + $ADODB_ACTIVE_CACHESECS - (abs(rand()) % 16) > time()) { + // abs(rand()) randomizes deletion, reducing contention to delete/refresh file + // ideally, you should cache at least 32 secs + $activedb->tables[$table] = $acttab; + + //if ($db->debug) ADOConnection::outp("Reading cached active record file: $fname"); + return; + } else if ($db->debug) { + ADOConnection::outp("Refreshing cached active record file: $fname"); + } + } + $activetab = new ADODB_Active_Table(); + $activetab->name = $table; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + if ($db->fetchMode !== false) { + $savem = $db->SetFetchMode(false); + } + + $cols = $db->MetaColumns($table); + + if (isset($savem)) { + $db->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + if (!$cols) { + $this->Error("Invalid table name: $table",'UpdateActiveTable'); + return false; + } + $fld = reset($cols); + if (!$pkeys) { + if (isset($fld->primary_key)) { + $pkeys = array(); + foreach($cols as $name => $fld) { + if (!empty($fld->primary_key)) { + $pkeys[] = $name; + } + } + } else { + $pkeys = $this->GetPrimaryKeys($db, $table); + } + } + if (empty($pkeys)) { + $this->Error("No primary key found for table $table",'UpdateActiveTable'); + return false; + } + + $attr = array(); + $keys = array(); + + switch($ADODB_ASSOC_CASE) { + case 0: + foreach($cols as $name => $fldobj) { + $name = strtolower($name); + if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) { + $this->$name = $fldobj->default_value; + } + else { + $this->$name = null; + } + $attr[$name] = $fldobj; + } + foreach($pkeys as $k => $name) { + $keys[strtolower($name)] = strtolower($name); + } + break; + + case 1: + foreach($cols as $name => $fldobj) { + $name = strtoupper($name); + + if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) { + $this->$name = $fldobj->default_value; + } + else { + $this->$name = null; + } + $attr[$name] = $fldobj; + } + + foreach($pkeys as $k => $name) { + $keys[strtoupper($name)] = strtoupper($name); + } + break; + default: + foreach($cols as $name => $fldobj) { + $name = ($fldobj->name); + + if ($ADODB_ACTIVE_DEFVALS && isset($fldobj->default_value)) { + $this->$name = $fldobj->default_value; + } + else { + $this->$name = null; + } + $attr[$name] = $fldobj; + } + foreach($pkeys as $k => $name) { + $keys[$name] = $cols[$name]->name; + } + break; + } + + $activetab->keys = $keys; + $activetab->flds = $attr; + $activetab->updateColsCount(); + + if ($ADODB_ACTIVE_CACHESECS && $ADODB_CACHE_DIR) { + $activetab->_created = time(); + $s = serialize($activetab); + if (!function_exists('adodb_write_file')) { + include(ADODB_DIR.'/adodb-csvlib.inc.php'); + } + adodb_write_file($fname,$s); + } + if (isset($activedb->tables[$table])) { + $oldtab = $activedb->tables[$table]; + + if ($oldtab) { + $activetab->_belongsTo = $oldtab->_belongsTo; + $activetab->_hasMany = $oldtab->_hasMany; + } + } + $activedb->tables[$table] = $activetab; + } + + function GetPrimaryKeys(&$db, $table) + { + return $db->MetaPrimaryKeys($table); + } + + // error handler for both PHP4+5. + function Error($err,$fn) + { + global $_ADODB_ACTIVE_DBS; + + $fn = get_class($this).'::'.$fn; + $this->_lasterr = $fn.': '.$err; + + if ($this->_dbat < 0) { + $db = false; + } + else { + $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; + $db = $activedb->db; + } + + if (function_exists('adodb_throw')) { + if (!$db) { + adodb_throw('ADOdb_Active_Record', $fn, -1, $err, 0, 0, false); + } + else { + adodb_throw($db->databaseType, $fn, -1, $err, 0, 0, $db); + } + } else { + if (!$db || $db->debug) { + ADOConnection::outp($this->_lasterr); + } + } + + } + + // return last error message + function ErrorMsg() + { + if (!function_exists('adodb_throw')) { + if ($this->_dbat < 0) { + $db = false; + } + else { + $db = $this->DB(); + } + + // last error could be database error too + if ($db && $db->ErrorMsg()) { + return $db->ErrorMsg(); + } + } + return $this->_lasterr; + } + + function ErrorNo() + { + if ($this->_dbat < 0) { + return -9999; // no database connection... + } + $db = $this->DB(); + + return (int) $db->ErrorNo(); + } + + + // retrieve ADOConnection from _ADODB_Active_DBs + function DB() + { + global $_ADODB_ACTIVE_DBS; + + if ($this->_dbat < 0) { + $false = false; + $this->Error("No database connection set: use ADOdb_Active_Record::SetDatabaseAdaptor(\$db)", "DB"); + return $false; + } + $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; + $db = $activedb->db; + return $db; + } + + // retrieve ADODB_Active_Table + function &TableInfo() + { + global $_ADODB_ACTIVE_DBS; + + $activedb = $_ADODB_ACTIVE_DBS[$this->_dbat]; + $table = $activedb->tables[$this->_tableat]; + return $table; + } + + + // I have an ON INSERT trigger on a table that sets other columns in the table. + // So, I find that for myTable, I want to reload an active record after saving it. -- Malcolm Cook + function Reload() + { + $db =& $this->DB(); + if (!$db) { + return false; + } + $table =& $this->TableInfo(); + $where = $this->GenWhere($db, $table); + return($this->Load($where)); + } + + + // set a numeric array (using natural table field ordering) as object properties + function Set(&$row) + { + global $ACTIVE_RECORD_SAFETY; + + $db = $this->DB(); + + if (!$row) { + $this->_saved = false; + return false; + } + + $this->_saved = true; + + $table = $this->TableInfo(); + $sizeofFlds = sizeof($table->flds); + $sizeofRow = sizeof($row); + if ($ACTIVE_RECORD_SAFETY && $table->_colsCount != $sizeofRow && $sizeofFlds != $sizeofRow) { + # + $bad_size = TRUE; + if($sizeofRow == 2 * $table->_colsCount || $sizeofRow == 2 * $sizeofFlds) { + // Only keep string keys + $keys = array_filter(array_keys($row), 'is_string'); + if (sizeof($keys) == sizeof($table->flds)) { + $bad_size = FALSE; + } + } + if ($bad_size) { + $this->Error("Table structure of $this->_table has changed","Load"); + return false; + } + # + } + else { + $keys = array_keys($row); + } + + # + reset($keys); + $this->_original = array(); + foreach($table->flds as $name=>$fld) { + $value = $row[current($keys)]; + $this->$name = $value; + $this->_original[] = $value; + if(!next($keys)) { + break; + } + } + $table =& $this->TableInfo(); + foreach($table->_belongsTo as $foreignTable) { + $ft = $foreignTable->TableInfo(); + $propertyName = $ft->name; + foreach($ft->flds as $name=>$fld) { + $value = $row[current($keys)]; + $foreignTable->$name = $value; + $foreignTable->_original[] = $value; + if(!next($keys)) { + break; + } + } + } + foreach($table->_hasMany as $foreignTable) { + $ft = $foreignTable->TableInfo(); + foreach($ft->flds as $name=>$fld) { + $value = $row[current($keys)]; + $foreignTable->$name = $value; + $foreignTable->_original[] = $value; + if(!next($keys)) { + break; + } + } + } + # + + return true; + } + + // get last inserted id for INSERT + function LastInsertID(&$db,$fieldname) + { + if ($db->hasInsertID) { + $val = $db->Insert_ID($this->_table,$fieldname); + } + else { + $val = false; + } + + if (is_null($val) || $val === false) { + // this might not work reliably in multi-user environment + return $db->GetOne("select max(".$fieldname.") from ".$this->_table); + } + return $val; + } + + // quote data in where clause + function doquote(&$db, $val,$t) + { + switch($t) { + case 'D': + case 'T': + if (empty($val)) { + return 'null'; + } + case 'C': + case 'X': + if (is_null($val)) { + return 'null'; + } + if (strlen($val)>0 && + (strncmp($val,"'",1) != 0 || substr($val,strlen($val)-1,1) != "'") + ) { + return $db->qstr($val); + break; + } + default: + return $val; + break; + } + } + + // generate where clause for an UPDATE/SELECT + function GenWhere(&$db, &$table) + { + $keys = $table->keys; + $parr = array(); + + foreach($keys as $k) { + $f = $table->flds[$k]; + if ($f) { + $parr[] = $k.' = '.$this->doquote($db,$this->$k,$db->MetaType($f->type)); + } + } + return implode(' and ', $parr); + } + + + //------------------------------------------------------------ Public functions below + + function Load($where=null,$bindarr=false) + { + $db = $this->DB(); + if (!$db) { + return false; + } + $this->_where = $where; + + $save = $db->SetFetchMode(ADODB_FETCH_NUM); + $qry = "select * from ".$this->_table; + $table =& $this->TableInfo(); + + if(($k = reset($table->keys))) { + $hasManyId = $k; + } + else { + $hasManyId = 'id'; + } + + foreach($table->_belongsTo as $foreignTable) { + if(($k = reset($foreignTable->TableInfo()->keys))) { + $belongsToId = $k; + } + else { + $belongsToId = 'id'; + } + $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '. + $this->_table.'.'.$foreignTable->foreignKey.'='. + $foreignTable->_table.'.'.$belongsToId; + } + foreach($table->_hasMany as $foreignTable) + { + $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '. + $this->_table.'.'.$hasManyId.'='. + $foreignTable->_table.'.'.$foreignTable->foreignKey; + } + if($where) { + $qry .= ' WHERE '.$where; + } + + // Simple case: no relations. Load row and return. + if((count($table->_hasMany) + count($table->_belongsTo)) < 1) { + $row = $db->GetRow($qry,$bindarr); + if(!$row) { + return false; + } + $db->SetFetchMode($save); + return $this->Set($row); + } + + // More complex case when relations have to be collated + $rows = $db->GetAll($qry,$bindarr); + if(!$rows) { + return false; + } + $db->SetFetchMode($save); + if(count($rows) < 1) { + return false; + } + $class = get_class($this); + $isFirstRow = true; + + if(($k = reset($this->TableInfo()->keys))) { + $myId = $k; + } + else { + $myId = 'id'; + } + $index = 0; $found = false; + /** @todo Improve by storing once and for all in table metadata */ + /** @todo Also re-use info for hasManyId */ + foreach($this->TableInfo()->flds as $fld) { + if($fld->name == $myId) { + $found = true; + break; + } + $index++; + } + if(!$found) { + $this->outp_throw("Unable to locate key $myId for $class in Load()",'Load'); + } + + foreach($rows as $row) { + $rowId = intval($row[$index]); + if($rowId > 0) { + if($isFirstRow) { + $isFirstRow = false; + if(!$this->Set($row)) { + return false; + } + } + $obj = new $class($table,false,$db); + $obj->Set($row); + // TODO Copy/paste code below: bad! + if(count($table->_hasMany) > 0) { + foreach($table->_hasMany as $foreignTable) { + $foreignName = $foreignTable->foreignName; + if(!empty($obj->$foreignName)) { + if(!is_array($this->$foreignName)) { + $foreignObj = $this->$foreignName; + $this->$foreignName = array(clone($foreignObj)); + } + else { + $foreignObj = $obj->$foreignName; + array_push($this->$foreignName, clone($foreignObj)); + } + } + } + } + if(count($table->_belongsTo) > 0) { + foreach($table->_belongsTo as $foreignTable) { + $foreignName = $foreignTable->foreignName; + if(!empty($obj->$foreignName)) { + if(!is_array($this->$foreignName)) { + $foreignObj = $this->$foreignName; + $this->$foreignName = array(clone($foreignObj)); + } + else { + $foreignObj = $obj->$foreignName; + array_push($this->$foreignName, clone($foreignObj)); + } + } + } + } + } + } + return true; + } + + // false on error + function Save() + { + if ($this->_saved) { + $ok = $this->Update(); + } + else { + $ok = $this->Insert(); + } + + return $ok; + } + + // CFR: Sometimes we may wish to consider that an object is not to be replaced but inserted. + // Sample use case: an 'undo' command object (after a delete()) + function Dirty() + { + $this->_saved = false; + } + + // false on error + function Insert() + { + $db = $this->DB(); + if (!$db) { + return false; + } + $cnt = 0; + $table = $this->TableInfo(); + + $valarr = array(); + $names = array(); + $valstr = array(); + + foreach($table->flds as $name=>$fld) { + $val = $this->$name; + if(!is_null($val) || !array_key_exists($name, $table->keys)) { + $valarr[] = $val; + $names[] = $name; + $valstr[] = $db->Param($cnt); + $cnt += 1; + } + } + + if (empty($names)){ + foreach($table->flds as $name=>$fld) { + $valarr[] = null; + $names[] = $name; + $valstr[] = $db->Param($cnt); + $cnt += 1; + } + } + $sql = 'INSERT INTO '.$this->_table."(".implode(',',$names).') VALUES ('.implode(',',$valstr).')'; + $ok = $db->Execute($sql,$valarr); + + if ($ok) { + $this->_saved = true; + $autoinc = false; + foreach($table->keys as $k) { + if (is_null($this->$k)) { + $autoinc = true; + break; + } + } + if ($autoinc && sizeof($table->keys) == 1) { + $k = reset($table->keys); + $this->$k = $this->LastInsertID($db,$k); + } + } + + $this->_original = $valarr; + return !empty($ok); + } + + function Delete() + { + $db = $this->DB(); + if (!$db) { + return false; + } + $table = $this->TableInfo(); + + $where = $this->GenWhere($db,$table); + $sql = 'DELETE FROM '.$this->_table.' WHERE '.$where; + $ok = $db->Execute($sql); + + return $ok ? true : false; + } + + // returns an array of active record objects + function Find($whereOrderBy,$bindarr=false,$pkeysArr=false,$extra=array()) + { + $db = $this->DB(); + if (!$db || empty($this->_table)) { + return false; + } + $table =& $this->TableInfo(); + $arr = $db->GetActiveRecordsClass(get_class($this),$this, $whereOrderBy,$bindarr,$pkeysArr,$extra, + array('foreignName'=>$this->foreignName, 'belongsTo'=>$table->_belongsTo, 'hasMany'=>$table->_hasMany)); + return $arr; + } + + // CFR: In introduced this method to ensure that inner workings are not disturbed by + // subclasses...for instance when GetActiveRecordsClass invokes Find() + // Why am I not invoking parent::Find? + // Shockingly because I want to preserve PHP4 compatibility. + function packageFind($whereOrderBy,$bindarr=false,$pkeysArr=false,$extra=array()) + { + $db = $this->DB(); + if (!$db || empty($this->_table)) { + return false; + } + $table =& $this->TableInfo(); + $arr = $db->GetActiveRecordsClass(get_class($this),$this, $whereOrderBy,$bindarr,$pkeysArr,$extra, + array('foreignName'=>$this->foreignName, 'belongsTo'=>$table->_belongsTo, 'hasMany'=>$table->_hasMany)); + return $arr; + } + + // returns 0 on error, 1 on update, 2 on insert + function Replace() + { + global $ADODB_ASSOC_CASE; + + $db = $this->DB(); + if (!$db) { + return false; + } + $table = $this->TableInfo(); + + $pkey = $table->keys; + + foreach($table->flds as $name=>$fld) { + $val = $this->$name; + /* + if (is_null($val)) { + if (isset($fld->not_null) && $fld->not_null) { + if (isset($fld->default_value) && strlen($fld->default_value)) { + continue; + } + else { + $this->Error("Cannot update null into $name","Replace"); + return false; + } + } + }*/ + if (is_null($val) && !empty($fld->auto_increment)) { + continue; + } + $t = $db->MetaType($fld->type); + $arr[$name] = $this->doquote($db,$val,$t); + $valarr[] = $val; + } + + if (!is_array($pkey)) { + $pkey = array($pkey); + } + + + switch ($ADODB_ASSOC_CASE == 0) { + case ADODB_ASSOC_CASE_LOWER: + foreach($pkey as $k => $v) { + $pkey[$k] = strtolower($v); + } + break; + case ADODB_ASSOC_CASE_UPPER: + foreach($pkey as $k => $v) { + $pkey[$k] = strtoupper($v); + } + break; + } + + $ok = $db->Replace($this->_table,$arr,$pkey); + if ($ok) { + $this->_saved = true; // 1= update 2=insert + if ($ok == 2) { + $autoinc = false; + foreach($table->keys as $k) { + if (is_null($this->$k)) { + $autoinc = true; + break; + } + } + if ($autoinc && sizeof($table->keys) == 1) { + $k = reset($table->keys); + $this->$k = $this->LastInsertID($db,$k); + } + } + + $this->_original = $valarr; + } + return $ok; + } + + // returns 0 on error, 1 on update, -1 if no change in data (no update) + function Update() + { + $db = $this->DB(); + if (!$db) { + return false; + } + $table = $this->TableInfo(); + + $where = $this->GenWhere($db, $table); + + if (!$where) { + $this->error("Where missing for table $table", "Update"); + return false; + } + $valarr = array(); + $neworig = array(); + $pairs = array(); + $i = -1; + $cnt = 0; + foreach($table->flds as $name=>$fld) { + $i += 1; + $val = $this->$name; + $neworig[] = $val; + + if (isset($table->keys[$name])) { + continue; + } + + if (is_null($val)) { + if (isset($fld->not_null) && $fld->not_null) { + if (isset($fld->default_value) && strlen($fld->default_value)) { + continue; + } + else { + $this->Error("Cannot set field $name to NULL","Update"); + return false; + } + } + } + + if (isset($this->_original[$i]) && $val === $this->_original[$i]) { + continue; + } + $valarr[] = $val; + $pairs[] = $name.'='.$db->Param($cnt); + $cnt += 1; + } + + + if (!$cnt) { + return -1; + } + $sql = 'UPDATE '.$this->_table." SET ".implode(",",$pairs)." WHERE ".$where; + $ok = $db->Execute($sql,$valarr); + if ($ok) { + $this->_original = $neworig; + return 1; + } + return 0; + } + + function GetAttributeNames() + { + $table = $this->TableInfo(); + if (!$table) { + return false; + } + return array_keys($table->flds); + } + +}; + +function adodb_GetActiveRecordsClass(&$db, $class, $tableObj,$whereOrderBy,$bindarr, $primkeyArr, + $extra, $relations) +{ + global $_ADODB_ACTIVE_DBS; + + if (empty($extra['loading'])) { + $extra['loading'] = ADODB_LAZY_AR; + } + $save = $db->SetFetchMode(ADODB_FETCH_NUM); + $table = &$tableObj->_table; + $tableInfo =& $tableObj->TableInfo(); + if(($k = reset($tableInfo->keys))) { + $myId = $k; + } + else { + $myId = 'id'; + } + $index = 0; $found = false; + /** @todo Improve by storing once and for all in table metadata */ + /** @todo Also re-use info for hasManyId */ + foreach($tableInfo->flds as $fld) + { + if($fld->name == $myId) { + $found = true; + break; + } + $index++; + } + if(!$found) { + $db->outp_throw("Unable to locate key $myId for $class in GetActiveRecordsClass()",'GetActiveRecordsClass'); + } + + $qry = "select * from ".$table; + if(ADODB_JOIN_AR == $extra['loading']) { + if(!empty($relations['belongsTo'])) { + foreach($relations['belongsTo'] as $foreignTable) { + if(($k = reset($foreignTable->TableInfo()->keys))) { + $belongsToId = $k; + } + else { + $belongsToId = 'id'; + } + + $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '. + $table.'.'.$foreignTable->foreignKey.'='. + $foreignTable->_table.'.'.$belongsToId; + } + } + if(!empty($relations['hasMany'])) { + if(empty($relations['foreignName'])) { + $db->outp_throw("Missing foreignName is relation specification in GetActiveRecordsClass()",'GetActiveRecordsClass'); + } + if(($k = reset($tableInfo->keys))) { + $hasManyId = $k; + } + else { + $hasManyId = 'id'; + } + + foreach($relations['hasMany'] as $foreignTable) { + $qry .= ' LEFT JOIN '.$foreignTable->_table.' ON '. + $table.'.'.$hasManyId.'='. + $foreignTable->_table.'.'.$foreignTable->foreignKey; + } + } + } + if (!empty($whereOrderBy)) { + $qry .= ' WHERE '.$whereOrderBy; + } + if(isset($extra['limit'])) { + $rows = false; + if(isset($extra['offset'])) { + $rs = $db->SelectLimit($qry, $extra['limit'], $extra['offset']); + } else { + $rs = $db->SelectLimit($qry, $extra['limit']); + } + if ($rs) { + while (!$rs->EOF) { + $rows[] = $rs->fields; + $rs->MoveNext(); + } + } + } else + $rows = $db->GetAll($qry,$bindarr); + + $db->SetFetchMode($save); + + $false = false; + + if ($rows === false) { + return $false; + } + + + if (!isset($_ADODB_ACTIVE_DBS)) { + include(ADODB_DIR.'/adodb-active-record.inc.php'); + } + if (!class_exists($class)) { + $db->outp_throw("Unknown class $class in GetActiveRecordsClass()",'GetActiveRecordsClass'); + return $false; + } + $uniqArr = array(); // CFR Keep track of records for relations + $arr = array(); + // arrRef will be the structure that knows about our objects. + // It is an associative array. + // We will, however, return arr, preserving regular 0.. order so that + // obj[0] can be used by app developpers. + $arrRef = array(); + $bTos = array(); // Will store belongTo's indices if any + foreach($rows as $row) { + + $obj = new $class($table,$primkeyArr,$db); + if ($obj->ErrorNo()){ + $db->_errorMsg = $obj->ErrorMsg(); + return $false; + } + $obj->Set($row); + // CFR: FIXME: Insane assumption here: + // If the first column returned is an integer, then it's a 'id' field + // And to make things a bit worse, I use intval() rather than is_int() because, in fact, + // $row[0] is not an integer. + // + // So, what does this whole block do? + // When relationships are found, we perform JOINs. This is fast. But not accurate: + // instead of returning n objects with their n' associated cousins, + // we get n*n' objects. This code fixes this. + // Note: to-many relationships mess around with the 'limit' parameter + $rowId = intval($row[$index]); + + if(ADODB_WORK_AR == $extra['loading']) { + $arrRef[$rowId] = $obj; + $arr[] = &$arrRef[$rowId]; + if(!isset($indices)) { + $indices = $rowId; + } + else { + $indices .= ','.$rowId; + } + if(!empty($relations['belongsTo'])) { + foreach($relations['belongsTo'] as $foreignTable) { + $foreignTableRef = $foreignTable->foreignKey; + // First array: list of foreign ids we are looking for + if(empty($bTos[$foreignTableRef])) { + $bTos[$foreignTableRef] = array(); + } + // Second array: list of ids found + if(empty($obj->$foreignTableRef)) { + continue; + } + if(empty($bTos[$foreignTableRef][$obj->$foreignTableRef])) { + $bTos[$foreignTableRef][$obj->$foreignTableRef] = array(); + } + $bTos[$foreignTableRef][$obj->$foreignTableRef][] = $obj; + } + } + continue; + } + + if($rowId>0) { + if(ADODB_JOIN_AR == $extra['loading']) { + $isNewObj = !isset($uniqArr['_'.$row[0]]); + if($isNewObj) { + $uniqArr['_'.$row[0]] = $obj; + } + + // TODO Copy/paste code below: bad! + if(!empty($relations['hasMany'])) { + foreach($relations['hasMany'] as $foreignTable) { + $foreignName = $foreignTable->foreignName; + if(!empty($obj->$foreignName)) { + $masterObj = &$uniqArr['_'.$row[0]]; + // Assumption: this property exists in every object since they are instances of the same class + if(!is_array($masterObj->$foreignName)) { + // Pluck! + $foreignObj = $masterObj->$foreignName; + $masterObj->$foreignName = array(clone($foreignObj)); + } + else { + // Pluck pluck! + $foreignObj = $obj->$foreignName; + array_push($masterObj->$foreignName, clone($foreignObj)); + } + } + } + } + if(!empty($relations['belongsTo'])) { + foreach($relations['belongsTo'] as $foreignTable) { + $foreignName = $foreignTable->foreignName; + if(!empty($obj->$foreignName)) { + $masterObj = &$uniqArr['_'.$row[0]]; + // Assumption: this property exists in every object since they are instances of the same class + if(!is_array($masterObj->$foreignName)) { + // Pluck! + $foreignObj = $masterObj->$foreignName; + $masterObj->$foreignName = array(clone($foreignObj)); + } + else { + // Pluck pluck! + $foreignObj = $obj->$foreignName; + array_push($masterObj->$foreignName, clone($foreignObj)); + } + } + } + } + if(!$isNewObj) { + unset($obj); // We do not need this object itself anymore and do not want it re-added to the main array + } + } + else if(ADODB_LAZY_AR == $extra['loading']) { + // Lazy loading: we need to give AdoDb a hint that we have not really loaded + // anything, all the while keeping enough information on what we wish to load. + // Let's do this by keeping the relevant info in our relationship arrays + // but get rid of the actual properties. + // We will then use PHP's __get to load these properties on-demand. + if(!empty($relations['hasMany'])) { + foreach($relations['hasMany'] as $foreignTable) { + $foreignName = $foreignTable->foreignName; + if(!empty($obj->$foreignName)) { + unset($obj->$foreignName); + } + } + } + if(!empty($relations['belongsTo'])) { + foreach($relations['belongsTo'] as $foreignTable) { + $foreignName = $foreignTable->foreignName; + if(!empty($obj->$foreignName)) { + unset($obj->$foreignName); + } + } + } + } + } + + if(isset($obj)) { + $arr[] = $obj; + } + } + + if(ADODB_WORK_AR == $extra['loading']) { + // The best of both worlds? + // Here, the number of queries is constant: 1 + n*relationship. + // The second query will allow us to perform a good join + // while preserving LIMIT etc. + if(!empty($relations['hasMany'])) { + foreach($relations['hasMany'] as $foreignTable) { + $foreignName = $foreignTable->foreignName; + $className = ucfirst($foreignTable->_singularize($foreignName)); + $obj = new $className(); + $dbClassRef = $foreignTable->foreignKey; + $objs = $obj->packageFind($dbClassRef.' IN ('.$indices.')'); + foreach($objs as $obj) { + if(!is_array($arrRef[$obj->$dbClassRef]->$foreignName)) { + $arrRef[$obj->$dbClassRef]->$foreignName = array(); + } + array_push($arrRef[$obj->$dbClassRef]->$foreignName, $obj); + } + } + + } + if(!empty($relations['belongsTo'])) { + foreach($relations['belongsTo'] as $foreignTable) { + $foreignTableRef = $foreignTable->foreignKey; + if(empty($bTos[$foreignTableRef])) { + continue; + } + if(($k = reset($foreignTable->TableInfo()->keys))) { + $belongsToId = $k; + } + else { + $belongsToId = 'id'; + } + $origObjsArr = $bTos[$foreignTableRef]; + $bTosString = implode(',', array_keys($bTos[$foreignTableRef])); + $foreignName = $foreignTable->foreignName; + $className = ucfirst($foreignTable->_singularize($foreignName)); + $obj = new $className(); + $objs = $obj->packageFind($belongsToId.' IN ('.$bTosString.')'); + foreach($objs as $obj) + { + foreach($origObjsArr[$obj->$belongsToId] as $idx=>$origObj) + { + $origObj->$foreignName = $obj; + } + } + } + } + } + + return $arr; +} diff --git a/app/vendor/adodb/adodb-php/adodb-csvlib.inc.php b/app/vendor/adodb/adodb-php/adodb-csvlib.inc.php new file mode 100644 index 000000000..bfd8d9b87 --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-csvlib.inc.php @@ -0,0 +1,319 @@ +FieldCount() : 0; + + if ($sql) $sql = urlencode($sql); + // metadata setup + + if ($max <= 0 || $rs->dataProvider == 'empty') { // is insert/update/delete + if (is_object($conn)) { + $sql .= ','.$conn->Affected_Rows(); + $sql .= ','.$conn->Insert_ID(); + } else + $sql .= ',,'; + + $text = "====-1,0,$sql\n"; + return $text; + } + $tt = ($rs->timeCreated) ? $rs->timeCreated : time(); + + ## changed format from ====0 to ====1 + $line = "====1,$tt,$sql\n"; + + if ($rs->databaseType == 'array') { + $rows = $rs->_array; + } else { + $rows = array(); + while (!$rs->EOF) { + $rows[] = $rs->fields; + $rs->MoveNext(); + } + } + + for($i=0; $i < $max; $i++) { + $o = $rs->FetchField($i); + $flds[] = $o; + } + + $savefetch = isset($rs->adodbFetchMode) ? $rs->adodbFetchMode : $rs->fetchMode; + $class = $rs->connection->arrayClass; + $rs2 = new $class(); + $rs2->timeCreated = $rs->timeCreated; # memcache fix + $rs2->sql = $rs->sql; + $rs2->oldProvider = $rs->dataProvider; + $rs2->InitArrayFields($rows,$flds); + $rs2->fetchMode = $savefetch; + return $line.serialize($rs2); + } + + +/** +* Open CSV file and convert it into Data. +* +* @param url file/ftp/http url +* @param err returns the error message +* @param timeout dispose if recordset has been alive for $timeout secs +* +* @return recordset, or false if error occured. If no +* error occurred in sql INSERT/UPDATE/DELETE, +* empty recordset is returned +*/ + function csv2rs($url,&$err,$timeout=0, $rsclass='ADORecordSet_array') + { + $false = false; + $err = false; + $fp = @fopen($url,'rb'); + if (!$fp) { + $err = $url.' file/URL not found'; + return $false; + } + @flock($fp, LOCK_SH); + $arr = array(); + $ttl = 0; + + if ($meta = fgetcsv($fp, 32000, ",")) { + // check if error message + if (strncmp($meta[0],'****',4) === 0) { + $err = trim(substr($meta[0],4,1024)); + fclose($fp); + return $false; + } + // check for meta data + // $meta[0] is -1 means return an empty recordset + // $meta[1] contains a time + + if (strncmp($meta[0], '====',4) === 0) { + + if ($meta[0] == "====-1") { + if (sizeof($meta) < 5) { + $err = "Corrupt first line for format -1"; + fclose($fp); + return $false; + } + fclose($fp); + + if ($timeout > 0) { + $err = " Illegal Timeout $timeout "; + return $false; + } + + $rs = new $rsclass($val=true); + $rs->fields = array(); + $rs->timeCreated = $meta[1]; + $rs->EOF = true; + $rs->_numOfFields = 0; + $rs->sql = urldecode($meta[2]); + $rs->affectedrows = (integer)$meta[3]; + $rs->insertid = $meta[4]; + return $rs; + } + # Under high volume loads, we want only 1 thread/process to _write_file + # so that we don't have 50 processes queueing to write the same data. + # We use probabilistic timeout, ahead of time. + # + # -4 sec before timeout, give processes 1/32 chance of timing out + # -2 sec before timeout, give processes 1/16 chance of timing out + # -1 sec after timeout give processes 1/4 chance of timing out + # +0 sec after timeout, give processes 100% chance of timing out + if (sizeof($meta) > 1) { + if($timeout >0){ + $tdiff = (integer)( $meta[1]+$timeout - time()); + if ($tdiff <= 2) { + switch($tdiff) { + case 4: + case 3: + if ((rand() & 31) == 0) { + fclose($fp); + $err = "Timeout 3"; + return $false; + } + break; + case 2: + if ((rand() & 15) == 0) { + fclose($fp); + $err = "Timeout 2"; + return $false; + } + break; + case 1: + if ((rand() & 3) == 0) { + fclose($fp); + $err = "Timeout 1"; + return $false; + } + break; + default: + fclose($fp); + $err = "Timeout 0"; + return $false; + } // switch + + } // if check flush cache + }// (timeout>0) + $ttl = $meta[1]; + } + //================================================ + // new cache format - use serialize extensively... + if ($meta[0] === '====1') { + // slurp in the data + $MAXSIZE = 128000; + + $text = fread($fp,$MAXSIZE); + if (strlen($text)) { + while ($txt = fread($fp,$MAXSIZE)) { + $text .= $txt; + } + } + fclose($fp); + $rs = unserialize($text); + if (is_object($rs)) $rs->timeCreated = $ttl; + else { + $err = "Unable to unserialize recordset"; + //echo htmlspecialchars($text),' !--END--!

'; + } + return $rs; + } + + $meta = false; + $meta = fgetcsv($fp, 32000, ","); + if (!$meta) { + fclose($fp); + $err = "Unexpected EOF 1"; + return $false; + } + } + + // Get Column definitions + $flds = array(); + foreach($meta as $o) { + $o2 = explode(':',$o); + if (sizeof($o2)!=3) { + $arr[] = $meta; + $flds = false; + break; + } + $fld = new ADOFieldObject(); + $fld->name = urldecode($o2[0]); + $fld->type = $o2[1]; + $fld->max_length = $o2[2]; + $flds[] = $fld; + } + } else { + fclose($fp); + $err = "Recordset had unexpected EOF 2"; + return $false; + } + + // slurp in the data + $MAXSIZE = 128000; + + $text = ''; + while ($txt = fread($fp,$MAXSIZE)) { + $text .= $txt; + } + + fclose($fp); + @$arr = unserialize($text); + //var_dump($arr); + if (!is_array($arr)) { + $err = "Recordset had unexpected EOF (in serialized recordset)"; + if (get_magic_quotes_runtime()) $err .= ". Magic Quotes Runtime should be disabled!"; + return $false; + } + $rs = new $rsclass(); + $rs->timeCreated = $ttl; + $rs->InitArrayFields($arr,$flds); + return $rs; + } + + + /** + * Save a file $filename and its $contents (normally for caching) with file locking + * Returns true if ok, false if fopen/fwrite error, 0 if rename error (eg. file is locked) + */ + function adodb_write_file($filename, $contents,$debug=false) + { + # http://www.php.net/bugs.php?id=9203 Bug that flock fails on Windows + # So to simulate locking, we assume that rename is an atomic operation. + # First we delete $filename, then we create a $tempfile write to it and + # rename to the desired $filename. If the rename works, then we successfully + # modified the file exclusively. + # What a stupid need - having to simulate locking. + # Risks: + # 1. $tempfile name is not unique -- very very low + # 2. unlink($filename) fails -- ok, rename will fail + # 3. adodb reads stale file because unlink fails -- ok, $rs timeout occurs + # 4. another process creates $filename between unlink() and rename() -- ok, rename() fails and cache updated + if (strncmp(PHP_OS,'WIN',3) === 0) { + // skip the decimal place + $mtime = substr(str_replace(' ','_',microtime()),2); + // getmypid() actually returns 0 on Win98 - never mind! + $tmpname = $filename.uniqid($mtime).getmypid(); + if (!($fd = @fopen($tmpname,'w'))) return false; + if (fwrite($fd,$contents)) $ok = true; + else $ok = false; + fclose($fd); + + if ($ok) { + @chmod($tmpname,0644); + // the tricky moment + @unlink($filename); + if (!@rename($tmpname,$filename)) { + @unlink($tmpname); + $ok = 0; + } + if (!$ok) { + if ($debug) ADOConnection::outp( " Rename $tmpname ".($ok? 'ok' : 'failed')); + } + } + return $ok; + } + if (!($fd = @fopen($filename, 'a'))) return false; + if (flock($fd, LOCK_EX) && ftruncate($fd, 0)) { + if (fwrite( $fd, $contents )) $ok = true; + else $ok = false; + fclose($fd); + @chmod($filename,0644); + }else { + fclose($fd); + if ($debug)ADOConnection::outp( " Failed acquiring lock for $filename
\n"); + $ok = false; + } + + return $ok; + } diff --git a/app/vendor/adodb/adodb-php/adodb-datadict.inc.php b/app/vendor/adodb/adodb-php/adodb-datadict.inc.php new file mode 100644 index 000000000..b15a80e66 --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-datadict.inc.php @@ -0,0 +1,1033 @@ +$str

"; +$a= Lens_ParseArgs($str); +print "
";
+print_r($a);
+print "
"; +} + + +if (!function_exists('ctype_alnum')) { + function ctype_alnum($text) { + return preg_match('/^[a-z0-9]*$/i', $text); + } +} + +//Lens_ParseTest(); + +/** + Parse arguments, treat "text" (text) and 'text' as quotation marks. + To escape, use "" or '' or )) + + Will read in "abc def" sans quotes, as: abc def + Same with 'abc def'. + However if `abc def`, then will read in as `abc def` + + @param endstmtchar Character that indicates end of statement + @param tokenchars Include the following characters in tokens apart from A-Z and 0-9 + @returns 2 dimensional array containing parsed tokens. +*/ +function Lens_ParseArgs($args,$endstmtchar=',',$tokenchars='_.-') +{ + $pos = 0; + $intoken = false; + $stmtno = 0; + $endquote = false; + $tokens = array(); + $tokens[$stmtno] = array(); + $max = strlen($args); + $quoted = false; + $tokarr = array(); + + while ($pos < $max) { + $ch = substr($args,$pos,1); + switch($ch) { + case ' ': + case "\t": + case "\n": + case "\r": + if (!$quoted) { + if ($intoken) { + $intoken = false; + $tokens[$stmtno][] = implode('',$tokarr); + } + break; + } + + $tokarr[] = $ch; + break; + + case '`': + if ($intoken) $tokarr[] = $ch; + case '(': + case ')': + case '"': + case "'": + + if ($intoken) { + if (empty($endquote)) { + $tokens[$stmtno][] = implode('',$tokarr); + if ($ch == '(') $endquote = ')'; + else $endquote = $ch; + $quoted = true; + $intoken = true; + $tokarr = array(); + } else if ($endquote == $ch) { + $ch2 = substr($args,$pos+1,1); + if ($ch2 == $endquote) { + $pos += 1; + $tokarr[] = $ch2; + } else { + $quoted = false; + $intoken = false; + $tokens[$stmtno][] = implode('',$tokarr); + $endquote = ''; + } + } else + $tokarr[] = $ch; + + }else { + + if ($ch == '(') $endquote = ')'; + else $endquote = $ch; + $quoted = true; + $intoken = true; + $tokarr = array(); + if ($ch == '`') $tokarr[] = '`'; + } + break; + + default: + + if (!$intoken) { + if ($ch == $endstmtchar) { + $stmtno += 1; + $tokens[$stmtno] = array(); + break; + } + + $intoken = true; + $quoted = false; + $endquote = false; + $tokarr = array(); + + } + + if ($quoted) $tokarr[] = $ch; + else if (ctype_alnum($ch) || strpos($tokenchars,$ch) !== false) $tokarr[] = $ch; + else { + if ($ch == $endstmtchar) { + $tokens[$stmtno][] = implode('',$tokarr); + $stmtno += 1; + $tokens[$stmtno] = array(); + $intoken = false; + $tokarr = array(); + break; + } + $tokens[$stmtno][] = implode('',$tokarr); + $tokens[$stmtno][] = $ch; + $intoken = false; + } + } + $pos += 1; + } + if ($intoken) $tokens[$stmtno][] = implode('',$tokarr); + + return $tokens; +} + + +class ADODB_DataDict { + var $connection; + var $debug = false; + var $dropTable = 'DROP TABLE %s'; + var $renameTable = 'RENAME TABLE %s TO %s'; + var $dropIndex = 'DROP INDEX %s'; + var $addCol = ' ADD'; + var $alterCol = ' ALTER COLUMN'; + var $dropCol = ' DROP COLUMN'; + var $renameColumn = 'ALTER TABLE %s RENAME COLUMN %s TO %s'; // table, old-column, new-column, column-definitions (not used by default) + var $nameRegex = '\w'; + var $nameRegexBrackets = 'a-zA-Z0-9_\(\)'; + var $schema = false; + var $serverInfo = array(); + var $autoIncrement = false; + var $dataProvider; + var $invalidResizeTypes4 = array('CLOB','BLOB','TEXT','DATE','TIME'); // for changetablesql + var $blobSize = 100; /// any varchar/char field this size or greater is treated as a blob + /// in other words, we use a text area for editting. + + function GetCommentSQL($table,$col) + { + return false; + } + + function SetCommentSQL($table,$col,$cmt) + { + return false; + } + + function MetaTables() + { + if (!$this->connection->IsConnected()) return array(); + return $this->connection->MetaTables(); + } + + function MetaColumns($tab, $upper=true, $schema=false) + { + if (!$this->connection->IsConnected()) return array(); + return $this->connection->MetaColumns($this->TableName($tab), $upper, $schema); + } + + function MetaPrimaryKeys($tab,$owner=false,$intkey=false) + { + if (!$this->connection->IsConnected()) return array(); + return $this->connection->MetaPrimaryKeys($this->TableName($tab), $owner, $intkey); + } + + function MetaIndexes($table, $primary = false, $owner = false) + { + if (!$this->connection->IsConnected()) return array(); + return $this->connection->MetaIndexes($this->TableName($table), $primary, $owner); + } + + function MetaType($t,$len=-1,$fieldobj=false) + { + static $typeMap = array( + 'VARCHAR' => 'C', + 'VARCHAR2' => 'C', + 'CHAR' => 'C', + 'C' => 'C', + 'STRING' => 'C', + 'NCHAR' => 'C', + 'NVARCHAR' => 'C', + 'VARYING' => 'C', + 'BPCHAR' => 'C', + 'CHARACTER' => 'C', + 'INTERVAL' => 'C', # Postgres + 'MACADDR' => 'C', # postgres + 'VAR_STRING' => 'C', # mysql + ## + 'LONGCHAR' => 'X', + 'TEXT' => 'X', + 'NTEXT' => 'X', + 'M' => 'X', + 'X' => 'X', + 'CLOB' => 'X', + 'NCLOB' => 'X', + 'LVARCHAR' => 'X', + ## + 'BLOB' => 'B', + 'IMAGE' => 'B', + 'BINARY' => 'B', + 'VARBINARY' => 'B', + 'LONGBINARY' => 'B', + 'B' => 'B', + ## + 'YEAR' => 'D', // mysql + 'DATE' => 'D', + 'D' => 'D', + ## + 'UNIQUEIDENTIFIER' => 'C', # MS SQL Server + ## + 'TIME' => 'T', + 'TIMESTAMP' => 'T', + 'DATETIME' => 'T', + 'TIMESTAMPTZ' => 'T', + 'SMALLDATETIME' => 'T', + 'T' => 'T', + 'TIMESTAMP WITHOUT TIME ZONE' => 'T', // postgresql + ## + 'BOOL' => 'L', + 'BOOLEAN' => 'L', + 'BIT' => 'L', + 'L' => 'L', + ## + 'COUNTER' => 'R', + 'R' => 'R', + 'SERIAL' => 'R', // ifx + 'INT IDENTITY' => 'R', + ## + 'INT' => 'I', + 'INT2' => 'I', + 'INT4' => 'I', + 'INT8' => 'I', + 'INTEGER' => 'I', + 'INTEGER UNSIGNED' => 'I', + 'SHORT' => 'I', + 'TINYINT' => 'I', + 'SMALLINT' => 'I', + 'I' => 'I', + ## + 'LONG' => 'N', // interbase is numeric, oci8 is blob + 'BIGINT' => 'N', // this is bigger than PHP 32-bit integers + 'DECIMAL' => 'N', + 'DEC' => 'N', + 'REAL' => 'N', + 'DOUBLE' => 'N', + 'DOUBLE PRECISION' => 'N', + 'SMALLFLOAT' => 'N', + 'FLOAT' => 'N', + 'NUMBER' => 'N', + 'NUM' => 'N', + 'NUMERIC' => 'N', + 'MONEY' => 'N', + + ## informix 9.2 + 'SQLINT' => 'I', + 'SQLSERIAL' => 'I', + 'SQLSMINT' => 'I', + 'SQLSMFLOAT' => 'N', + 'SQLFLOAT' => 'N', + 'SQLMONEY' => 'N', + 'SQLDECIMAL' => 'N', + 'SQLDATE' => 'D', + 'SQLVCHAR' => 'C', + 'SQLCHAR' => 'C', + 'SQLDTIME' => 'T', + 'SQLINTERVAL' => 'N', + 'SQLBYTES' => 'B', + 'SQLTEXT' => 'X', + ## informix 10 + "SQLINT8" => 'I8', + "SQLSERIAL8" => 'I8', + "SQLNCHAR" => 'C', + "SQLNVCHAR" => 'C', + "SQLLVARCHAR" => 'X', + "SQLBOOL" => 'L' + ); + + if (!$this->connection->IsConnected()) { + $t = strtoupper($t); + if (isset($typeMap[$t])) return $typeMap[$t]; + return 'N'; + } + return $this->connection->MetaType($t,$len,$fieldobj); + } + + function NameQuote($name = NULL,$allowBrackets=false) + { + if (!is_string($name)) { + return FALSE; + } + + $name = trim($name); + + if ( !is_object($this->connection) ) { + return $name; + } + + $quote = $this->connection->nameQuote; + + // if name is of the form `name`, quote it + if ( preg_match('/^`(.+)`$/', $name, $matches) ) { + return $quote . $matches[1] . $quote; + } + + // if name contains special characters, quote it + $regex = ($allowBrackets) ? $this->nameRegexBrackets : $this->nameRegex; + + if ( !preg_match('/^[' . $regex . ']+$/', $name) ) { + return $quote . $name . $quote; + } + + return $name; + } + + function TableName($name) + { + if ( $this->schema ) { + return $this->NameQuote($this->schema) .'.'. $this->NameQuote($name); + } + return $this->NameQuote($name); + } + + // Executes the sql array returned by GetTableSQL and GetIndexSQL + function ExecuteSQLArray($sql, $continueOnError = true) + { + $rez = 2; + $conn = $this->connection; + $saved = $conn->debug; + foreach($sql as $line) { + + if ($this->debug) $conn->debug = true; + $ok = $conn->Execute($line); + $conn->debug = $saved; + if (!$ok) { + if ($this->debug) ADOConnection::outp($conn->ErrorMsg()); + if (!$continueOnError) return 0; + $rez = 1; + } + } + return $rez; + } + + /** + Returns the actual type given a character code. + + C: varchar + X: CLOB (character large object) or largest varchar size if CLOB is not supported + C2: Multibyte varchar + X2: Multibyte CLOB + + B: BLOB (binary large object) + + D: Date + T: Date-time + L: Integer field suitable for storing booleans (0 or 1) + I: Integer + F: Floating point number + N: Numeric or decimal number + */ + + function ActualType($meta) + { + return $meta; + } + + function CreateDatabase($dbname,$options=false) + { + $options = $this->_Options($options); + $sql = array(); + + $s = 'CREATE DATABASE ' . $this->NameQuote($dbname); + if (isset($options[$this->upperName])) + $s .= ' '.$options[$this->upperName]; + + $sql[] = $s; + return $sql; + } + + /* + Generates the SQL to create index. Returns an array of sql strings. + */ + function CreateIndexSQL($idxname, $tabname, $flds, $idxoptions = false) + { + if (!is_array($flds)) { + $flds = explode(',',$flds); + } + + foreach($flds as $key => $fld) { + # some indexes can use partial fields, eg. index first 32 chars of "name" with NAME(32) + $flds[$key] = $this->NameQuote($fld,$allowBrackets=true); + } + + return $this->_IndexSQL($this->NameQuote($idxname), $this->TableName($tabname), $flds, $this->_Options($idxoptions)); + } + + function DropIndexSQL ($idxname, $tabname = NULL) + { + return array(sprintf($this->dropIndex, $this->NameQuote($idxname), $this->TableName($tabname))); + } + + function SetSchema($schema) + { + $this->schema = $schema; + } + + function AddColumnSQL($tabname, $flds) + { + $tabname = $this->TableName ($tabname); + $sql = array(); + list($lines,$pkey,$idxs) = $this->_GenFields($flds); + // genfields can return FALSE at times + if ($lines == null) $lines = array(); + $alter = 'ALTER TABLE ' . $tabname . $this->addCol . ' '; + foreach($lines as $v) { + $sql[] = $alter . $v; + } + if (is_array($idxs)) { + foreach($idxs as $idx => $idxdef) { + $sql_idxs = $this->CreateIndexSql($idx, $tabname, $idxdef['cols'], $idxdef['opts']); + $sql = array_merge($sql, $sql_idxs); + } + } + return $sql; + } + + /** + * Change the definition of one column + * + * As some DBM's can't do that on there own, you need to supply the complete defintion of the new table, + * to allow, recreating the table and copying the content over to the new table + * @param string $tabname table-name + * @param string $flds column-name and type for the changed column + * @param string $tableflds='' complete defintion of the new table, eg. for postgres, default '' + * @param array/string $tableoptions='' options for the new table see CreateTableSQL, default '' + * @return array with SQL strings + */ + function AlterColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + $tabname = $this->TableName ($tabname); + $sql = array(); + list($lines,$pkey,$idxs) = $this->_GenFields($flds); + // genfields can return FALSE at times + if ($lines == null) $lines = array(); + $alter = 'ALTER TABLE ' . $tabname . $this->alterCol . ' '; + foreach($lines as $v) { + $sql[] = $alter . $v; + } + if (is_array($idxs)) { + foreach($idxs as $idx => $idxdef) { + $sql_idxs = $this->CreateIndexSql($idx, $tabname, $idxdef['cols'], $idxdef['opts']); + $sql = array_merge($sql, $sql_idxs); + } + + } + return $sql; + } + + /** + * Rename one column + * + * Some DBM's can only do this together with changeing the type of the column (even if that stays the same, eg. mysql) + * @param string $tabname table-name + * @param string $oldcolumn column-name to be renamed + * @param string $newcolumn new column-name + * @param string $flds='' complete column-defintion-string like for AddColumnSQL, only used by mysql atm., default='' + * @return array with SQL strings + */ + function RenameColumnSQL($tabname,$oldcolumn,$newcolumn,$flds='') + { + $tabname = $this->TableName ($tabname); + if ($flds) { + list($lines,$pkey,$idxs) = $this->_GenFields($flds); + // genfields can return FALSE at times + if ($lines == null) $lines = array(); + list(,$first) = each($lines); + list(,$column_def) = preg_split("/[\t ]+/",$first,2); + } + return array(sprintf($this->renameColumn,$tabname,$this->NameQuote($oldcolumn),$this->NameQuote($newcolumn),$column_def)); + } + + /** + * Drop one column + * + * Some DBM's can't do that on there own, you need to supply the complete defintion of the new table, + * to allow, recreating the table and copying the content over to the new table + * @param string $tabname table-name + * @param string $flds column-name and type for the changed column + * @param string $tableflds='' complete defintion of the new table, eg. for postgres, default '' + * @param array/string $tableoptions='' options for the new table see CreateTableSQL, default '' + * @return array with SQL strings + */ + function DropColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + $tabname = $this->TableName ($tabname); + if (!is_array($flds)) $flds = explode(',',$flds); + $sql = array(); + $alter = 'ALTER TABLE ' . $tabname . $this->dropCol . ' '; + foreach($flds as $v) { + $sql[] = $alter . $this->NameQuote($v); + } + return $sql; + } + + function DropTableSQL($tabname) + { + return array (sprintf($this->dropTable, $this->TableName($tabname))); + } + + function RenameTableSQL($tabname,$newname) + { + return array (sprintf($this->renameTable, $this->TableName($tabname),$this->TableName($newname))); + } + + /** + Generate the SQL to create table. Returns an array of sql strings. + */ + function CreateTableSQL($tabname, $flds, $tableoptions=array()) + { + list($lines,$pkey,$idxs) = $this->_GenFields($flds, true); + // genfields can return FALSE at times + if ($lines == null) $lines = array(); + + $taboptions = $this->_Options($tableoptions); + $tabname = $this->TableName ($tabname); + $sql = $this->_TableSQL($tabname,$lines,$pkey,$taboptions); + + // ggiunta - 2006/10/12 - KLUDGE: + // if we are on autoincrement, and table options includes REPLACE, the + // autoincrement sequence has already been dropped on table creation sql, so + // we avoid passing REPLACE to trigger creation code. This prevents + // creating sql that double-drops the sequence + if ($this->autoIncrement && isset($taboptions['REPLACE'])) + unset($taboptions['REPLACE']); + $tsql = $this->_Triggers($tabname,$taboptions); + foreach($tsql as $s) $sql[] = $s; + + if (is_array($idxs)) { + foreach($idxs as $idx => $idxdef) { + $sql_idxs = $this->CreateIndexSql($idx, $tabname, $idxdef['cols'], $idxdef['opts']); + $sql = array_merge($sql, $sql_idxs); + } + } + + return $sql; + } + + + + function _GenFields($flds,$widespacing=false) + { + if (is_string($flds)) { + $padding = ' '; + $txt = $flds.$padding; + $flds = array(); + $flds0 = Lens_ParseArgs($txt,','); + $hasparam = false; + foreach($flds0 as $f0) { + $f1 = array(); + foreach($f0 as $token) { + switch (strtoupper($token)) { + case 'INDEX': + $f1['INDEX'] = ''; + // fall through intentionally + case 'CONSTRAINT': + case 'DEFAULT': + $hasparam = $token; + break; + default: + if ($hasparam) $f1[$hasparam] = $token; + else $f1[] = $token; + $hasparam = false; + break; + } + } + // 'index' token without a name means single column index: name it after column + if (array_key_exists('INDEX', $f1) && $f1['INDEX'] == '') { + $f1['INDEX'] = isset($f0['NAME']) ? $f0['NAME'] : $f0[0]; + // check if column name used to create an index name was quoted + if (($f1['INDEX'][0] == '"' || $f1['INDEX'][0] == "'" || $f1['INDEX'][0] == "`") && + ($f1['INDEX'][0] == substr($f1['INDEX'], -1))) { + $f1['INDEX'] = $f1['INDEX'][0].'idx_'.substr($f1['INDEX'], 1, -1).$f1['INDEX'][0]; + } + else + $f1['INDEX'] = 'idx_'.$f1['INDEX']; + } + // reset it, so we don't get next field 1st token as INDEX... + $hasparam = false; + + $flds[] = $f1; + + } + } + $this->autoIncrement = false; + $lines = array(); + $pkey = array(); + $idxs = array(); + foreach($flds as $fld) { + $fld = _array_change_key_case($fld); + + $fname = false; + $fdefault = false; + $fautoinc = false; + $ftype = false; + $fsize = false; + $fprec = false; + $fprimary = false; + $fnoquote = false; + $fdefts = false; + $fdefdate = false; + $fconstraint = false; + $fnotnull = false; + $funsigned = false; + $findex = ''; + $funiqueindex = false; + + //----------------- + // Parse attributes + foreach($fld as $attr => $v) { + if ($attr == 2 && is_numeric($v)) $attr = 'SIZE'; + else if (is_numeric($attr) && $attr > 1 && !is_numeric($v)) $attr = strtoupper($v); + + switch($attr) { + case '0': + case 'NAME': $fname = $v; break; + case '1': + case 'TYPE': $ty = $v; $ftype = $this->ActualType(strtoupper($v)); break; + + case 'SIZE': + $dotat = strpos($v,'.'); if ($dotat === false) $dotat = strpos($v,','); + if ($dotat === false) $fsize = $v; + else { + $fsize = substr($v,0,$dotat); + $fprec = substr($v,$dotat+1); + } + break; + case 'UNSIGNED': $funsigned = true; break; + case 'AUTOINCREMENT': + case 'AUTO': $fautoinc = true; $fnotnull = true; break; + case 'KEY': + // a primary key col can be non unique in itself (if key spans many cols...) + case 'PRIMARY': $fprimary = $v; $fnotnull = true; /*$funiqueindex = true;*/ break; + case 'DEF': + case 'DEFAULT': $fdefault = $v; break; + case 'NOTNULL': $fnotnull = $v; break; + case 'NOQUOTE': $fnoquote = $v; break; + case 'DEFDATE': $fdefdate = $v; break; + case 'DEFTIMESTAMP': $fdefts = $v; break; + case 'CONSTRAINT': $fconstraint = $v; break; + // let INDEX keyword create a 'very standard' index on column + case 'INDEX': $findex = $v; break; + case 'UNIQUE': $funiqueindex = true; break; + } //switch + } // foreach $fld + + //-------------------- + // VALIDATE FIELD INFO + if (!strlen($fname)) { + if ($this->debug) ADOConnection::outp("Undefined NAME"); + return false; + } + + $fid = strtoupper(preg_replace('/^`(.+)`$/', '$1', $fname)); + $fname = $this->NameQuote($fname); + + if (!strlen($ftype)) { + if ($this->debug) ADOConnection::outp("Undefined TYPE for field '$fname'"); + return false; + } else { + $ftype = strtoupper($ftype); + } + + $ftype = $this->_GetSize($ftype, $ty, $fsize, $fprec); + + if ($ty == 'X' || $ty == 'X2' || $ty == 'B') $fnotnull = false; // some blob types do not accept nulls + + if ($fprimary) $pkey[] = $fname; + + // some databases do not allow blobs to have defaults + if ($ty == 'X') $fdefault = false; + + // build list of indexes + if ($findex != '') { + if (array_key_exists($findex, $idxs)) { + $idxs[$findex]['cols'][] = ($fname); + if (in_array('UNIQUE', $idxs[$findex]['opts']) != $funiqueindex) { + if ($this->debug) ADOConnection::outp("Index $findex defined once UNIQUE and once not"); + } + if ($funiqueindex && !in_array('UNIQUE', $idxs[$findex]['opts'])) + $idxs[$findex]['opts'][] = 'UNIQUE'; + } + else + { + $idxs[$findex] = array(); + $idxs[$findex]['cols'] = array($fname); + if ($funiqueindex) + $idxs[$findex]['opts'] = array('UNIQUE'); + else + $idxs[$findex]['opts'] = array(); + } + } + + //-------------------- + // CONSTRUCT FIELD SQL + if ($fdefts) { + if (substr($this->connection->databaseType,0,5) == 'mysql') { + $ftype = 'TIMESTAMP'; + } else { + $fdefault = $this->connection->sysTimeStamp; + } + } else if ($fdefdate) { + if (substr($this->connection->databaseType,0,5) == 'mysql') { + $ftype = 'TIMESTAMP'; + } else { + $fdefault = $this->connection->sysDate; + } + } else if ($fdefault !== false && !$fnoquote) { + if ($ty == 'C' or $ty == 'X' or + ( substr($fdefault,0,1) != "'" && !is_numeric($fdefault))) { + + if (($ty == 'D' || $ty == 'T') && strtolower($fdefault) != 'null') { + // convert default date into database-aware code + if ($ty == 'T') + { + $fdefault = $this->connection->DBTimeStamp($fdefault); + } + else + { + $fdefault = $this->connection->DBDate($fdefault); + } + } + else + if (strlen($fdefault) != 1 && substr($fdefault,0,1) == ' ' && substr($fdefault,strlen($fdefault)-1) == ' ') + $fdefault = trim($fdefault); + else if (strtolower($fdefault) != 'null') + $fdefault = $this->connection->qstr($fdefault); + } + } + $suffix = $this->_CreateSuffix($fname,$ftype,$fnotnull,$fdefault,$fautoinc,$fconstraint,$funsigned); + + // add index creation + if ($widespacing) $fname = str_pad($fname,24); + + // check for field names appearing twice + if (array_key_exists($fid, $lines)) { + ADOConnection::outp("Field '$fname' defined twice"); + } + + $lines[$fid] = $fname.' '.$ftype.$suffix; + + if ($fautoinc) $this->autoIncrement = true; + } // foreach $flds + + return array($lines,$pkey,$idxs); + } + + /** + GENERATE THE SIZE PART OF THE DATATYPE + $ftype is the actual type + $ty is the type defined originally in the DDL + */ + function _GetSize($ftype, $ty, $fsize, $fprec) + { + if (strlen($fsize) && $ty != 'X' && $ty != 'B' && strpos($ftype,'(') === false) { + $ftype .= "(".$fsize; + if (strlen($fprec)) $ftype .= ",".$fprec; + $ftype .= ')'; + } + return $ftype; + } + + + // return string must begin with space + function _CreateSuffix($fname,&$ftype,$fnotnull,$fdefault,$fautoinc,$fconstraint,$funsigned) + { + $suffix = ''; + if (strlen($fdefault)) $suffix .= " DEFAULT $fdefault"; + if ($fnotnull) $suffix .= ' NOT NULL'; + if ($fconstraint) $suffix .= ' '.$fconstraint; + return $suffix; + } + + function _IndexSQL($idxname, $tabname, $flds, $idxoptions) + { + $sql = array(); + + if ( isset($idxoptions['REPLACE']) || isset($idxoptions['DROP']) ) { + $sql[] = sprintf ($this->dropIndex, $idxname); + if ( isset($idxoptions['DROP']) ) + return $sql; + } + + if ( empty ($flds) ) { + return $sql; + } + + $unique = isset($idxoptions['UNIQUE']) ? ' UNIQUE' : ''; + + $s = 'CREATE' . $unique . ' INDEX ' . $idxname . ' ON ' . $tabname . ' '; + + if ( isset($idxoptions[$this->upperName]) ) + $s .= $idxoptions[$this->upperName]; + + if ( is_array($flds) ) + $flds = implode(', ',$flds); + $s .= '(' . $flds . ')'; + $sql[] = $s; + + return $sql; + } + + function _DropAutoIncrement($tabname) + { + return false; + } + + function _TableSQL($tabname,$lines,$pkey,$tableoptions) + { + $sql = array(); + + if (isset($tableoptions['REPLACE']) || isset ($tableoptions['DROP'])) { + $sql[] = sprintf($this->dropTable,$tabname); + if ($this->autoIncrement) { + $sInc = $this->_DropAutoIncrement($tabname); + if ($sInc) $sql[] = $sInc; + } + if ( isset ($tableoptions['DROP']) ) { + return $sql; + } + } + $s = "CREATE TABLE $tabname (\n"; + $s .= implode(",\n", $lines); + if (sizeof($pkey)>0) { + $s .= ",\n PRIMARY KEY ("; + $s .= implode(", ",$pkey).")"; + } + if (isset($tableoptions['CONSTRAINTS'])) + $s .= "\n".$tableoptions['CONSTRAINTS']; + + if (isset($tableoptions[$this->upperName.'_CONSTRAINTS'])) + $s .= "\n".$tableoptions[$this->upperName.'_CONSTRAINTS']; + + $s .= "\n)"; + if (isset($tableoptions[$this->upperName])) $s .= $tableoptions[$this->upperName]; + $sql[] = $s; + + return $sql; + } + + /** + GENERATE TRIGGERS IF NEEDED + used when table has auto-incrementing field that is emulated using triggers + */ + function _Triggers($tabname,$taboptions) + { + return array(); + } + + /** + Sanitize options, so that array elements with no keys are promoted to keys + */ + function _Options($opts) + { + if (!is_array($opts)) return array(); + $newopts = array(); + foreach($opts as $k => $v) { + if (is_numeric($k)) $newopts[strtoupper($v)] = $v; + else $newopts[strtoupper($k)] = $v; + } + return $newopts; + } + + + function _getSizePrec($size) + { + $fsize = false; + $fprec = false; + $dotat = strpos($size,'.'); + if ($dotat === false) $dotat = strpos($size,','); + if ($dotat === false) $fsize = $size; + else { + $fsize = substr($size,0,$dotat); + $fprec = substr($size,$dotat+1); + } + return array($fsize, $fprec); + } + + /** + "Florian Buzin [ easywe ]" + + This function changes/adds new fields to your table. You don't + have to know if the col is new or not. It will check on its own. + */ + function ChangeTableSQL($tablename, $flds, $tableoptions = false, $dropOldFlds=false) + { + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + if ($this->connection->fetchMode !== false) $savem = $this->connection->SetFetchMode(false); + + // check table exists + $save_handler = $this->connection->raiseErrorFn; + $this->connection->raiseErrorFn = ''; + $cols = $this->MetaColumns($tablename); + $this->connection->raiseErrorFn = $save_handler; + + if (isset($savem)) $this->connection->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + if ( empty($cols)) { + return $this->CreateTableSQL($tablename, $flds, $tableoptions); + } + + if (is_array($flds)) { + // Cycle through the update fields, comparing + // existing fields to fields to update. + // if the Metatype and size is exactly the + // same, ignore - by Mark Newham + $holdflds = array(); + foreach($flds as $k=>$v) { + if ( isset($cols[$k]) && is_object($cols[$k]) ) { + // If already not allowing nulls, then don't change + $obj = $cols[$k]; + if (isset($obj->not_null) && $obj->not_null) + $v = str_replace('NOT NULL','',$v); + if (isset($obj->auto_increment) && $obj->auto_increment && empty($v['AUTOINCREMENT'])) + $v = str_replace('AUTOINCREMENT','',$v); + + $c = $cols[$k]; + $ml = $c->max_length; + $mt = $this->MetaType($c->type,$ml); + + if (isset($c->scale)) $sc = $c->scale; + else $sc = 99; // always force change if scale not known. + + if ($sc == -1) $sc = false; + list($fsize, $fprec) = $this->_getSizePrec($v['SIZE']); + + if ($ml == -1) $ml = ''; + if ($mt == 'X') $ml = $v['SIZE']; + if (($mt != $v['TYPE']) || ($ml != $fsize || $sc != $fprec) || (isset($v['AUTOINCREMENT']) && $v['AUTOINCREMENT'] != $obj->auto_increment)) { + $holdflds[$k] = $v; + } + } else { + $holdflds[$k] = $v; + } + } + $flds = $holdflds; + } + + + // already exists, alter table instead + list($lines,$pkey,$idxs) = $this->_GenFields($flds); + // genfields can return FALSE at times + if ($lines == null) $lines = array(); + $alter = 'ALTER TABLE ' . $this->TableName($tablename); + $sql = array(); + + foreach ( $lines as $id => $v ) { + if ( isset($cols[$id]) && is_object($cols[$id]) ) { + + $flds = Lens_ParseArgs($v,','); + + // We are trying to change the size of the field, if not allowed, simply ignore the request. + // $flds[1] holds the type, $flds[2] holds the size -postnuke addition + if ($flds && in_array(strtoupper(substr($flds[0][1],0,4)),$this->invalidResizeTypes4) + && (isset($flds[0][2]) && is_numeric($flds[0][2]))) { + if ($this->debug) ADOConnection::outp(sprintf("

%s cannot be changed to %s currently

", $flds[0][0], $flds[0][1])); + #echo "

$this->alterCol cannot be changed to $flds currently

"; + continue; + } + $sql[] = $alter . $this->alterCol . ' ' . $v; + } else { + $sql[] = $alter . $this->addCol . ' ' . $v; + } + } + + if ($dropOldFlds) { + foreach ( $cols as $id => $v ) + if ( !isset($lines[$id]) ) + $sql[] = $alter . $this->dropCol . ' ' . $v->name; + } + return $sql; + } +} // class diff --git a/app/vendor/adodb/adodb-php/adodb-error.inc.php b/app/vendor/adodb/adodb-php/adodb-error.inc.php new file mode 100644 index 000000000..d42a06a83 --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-error.inc.php @@ -0,0 +1,265 @@ + DB_ERROR_NOSUCHTABLE, + 'Relation [\"\'].*[\"\'] already exists|Cannot insert a duplicate key into (a )?unique index.*|duplicate key.*violates unique constraint' => DB_ERROR_ALREADY_EXISTS, + 'database ".+" does not exist$' => DB_ERROR_NOSUCHDB, + '(divide|division) by zero$' => DB_ERROR_DIVZERO, + 'pg_atoi: error in .*: can\'t parse ' => DB_ERROR_INVALID_NUMBER, + 'ttribute [\"\'].*[\"\'] not found|Relation [\"\'].*[\"\'] does not have attribute [\"\'].*[\"\']' => DB_ERROR_NOSUCHFIELD, + '(parser: parse|syntax) error at or near \"' => DB_ERROR_SYNTAX, + 'referential integrity violation' => DB_ERROR_CONSTRAINT, + 'deadlock detected$' => DB_ERROR_DEADLOCK, + 'canceling statement due to statement timeout$' => DB_ERROR_STATEMENT_TIMEOUT, + 'could not serialize access due to' => DB_ERROR_SERIALIZATION_FAILURE + ); + reset($error_regexps); + while (list($regexp,$code) = each($error_regexps)) { + if (preg_match("/$regexp/mi", $errormsg)) { + return $code; + } + } + // Fall back to DB_ERROR if there was no mapping. + return DB_ERROR; +} + +function adodb_error_odbc() +{ +static $MAP = array( + '01004' => DB_ERROR_TRUNCATED, + '07001' => DB_ERROR_MISMATCH, + '21S01' => DB_ERROR_MISMATCH, + '21S02' => DB_ERROR_MISMATCH, + '22003' => DB_ERROR_INVALID_NUMBER, + '22008' => DB_ERROR_INVALID_DATE, + '22012' => DB_ERROR_DIVZERO, + '23000' => DB_ERROR_CONSTRAINT, + '24000' => DB_ERROR_INVALID, + '34000' => DB_ERROR_INVALID, + '37000' => DB_ERROR_SYNTAX, + '42000' => DB_ERROR_SYNTAX, + 'IM001' => DB_ERROR_UNSUPPORTED, + 'S0000' => DB_ERROR_NOSUCHTABLE, + 'S0001' => DB_ERROR_NOT_FOUND, + 'S0002' => DB_ERROR_NOSUCHTABLE, + 'S0011' => DB_ERROR_ALREADY_EXISTS, + 'S0012' => DB_ERROR_NOT_FOUND, + 'S0021' => DB_ERROR_ALREADY_EXISTS, + 'S0022' => DB_ERROR_NOT_FOUND, + 'S1000' => DB_ERROR_NOSUCHTABLE, + 'S1009' => DB_ERROR_INVALID, + 'S1090' => DB_ERROR_INVALID, + 'S1C00' => DB_ERROR_NOT_CAPABLE + ); + return $MAP; +} + +function adodb_error_ibase() +{ +static $MAP = array( + -104 => DB_ERROR_SYNTAX, + -150 => DB_ERROR_ACCESS_VIOLATION, + -151 => DB_ERROR_ACCESS_VIOLATION, + -155 => DB_ERROR_NOSUCHTABLE, + -157 => DB_ERROR_NOSUCHFIELD, + -158 => DB_ERROR_VALUE_COUNT_ON_ROW, + -170 => DB_ERROR_MISMATCH, + -171 => DB_ERROR_MISMATCH, + -172 => DB_ERROR_INVALID, + -204 => DB_ERROR_INVALID, + -205 => DB_ERROR_NOSUCHFIELD, + -206 => DB_ERROR_NOSUCHFIELD, + -208 => DB_ERROR_INVALID, + -219 => DB_ERROR_NOSUCHTABLE, + -297 => DB_ERROR_CONSTRAINT, + -530 => DB_ERROR_CONSTRAINT, + -803 => DB_ERROR_CONSTRAINT, + -551 => DB_ERROR_ACCESS_VIOLATION, + -552 => DB_ERROR_ACCESS_VIOLATION, + -922 => DB_ERROR_NOSUCHDB, + -923 => DB_ERROR_CONNECT_FAILED, + -924 => DB_ERROR_CONNECT_FAILED + ); + + return $MAP; +} + +function adodb_error_ifx() +{ +static $MAP = array( + '-201' => DB_ERROR_SYNTAX, + '-206' => DB_ERROR_NOSUCHTABLE, + '-217' => DB_ERROR_NOSUCHFIELD, + '-329' => DB_ERROR_NODBSELECTED, + '-1204' => DB_ERROR_INVALID_DATE, + '-1205' => DB_ERROR_INVALID_DATE, + '-1206' => DB_ERROR_INVALID_DATE, + '-1209' => DB_ERROR_INVALID_DATE, + '-1210' => DB_ERROR_INVALID_DATE, + '-1212' => DB_ERROR_INVALID_DATE + ); + + return $MAP; +} + +function adodb_error_oci8() +{ +static $MAP = array( + 1 => DB_ERROR_ALREADY_EXISTS, + 900 => DB_ERROR_SYNTAX, + 904 => DB_ERROR_NOSUCHFIELD, + 923 => DB_ERROR_SYNTAX, + 942 => DB_ERROR_NOSUCHTABLE, + 955 => DB_ERROR_ALREADY_EXISTS, + 1476 => DB_ERROR_DIVZERO, + 1722 => DB_ERROR_INVALID_NUMBER, + 2289 => DB_ERROR_NOSUCHTABLE, + 2291 => DB_ERROR_CONSTRAINT, + 2449 => DB_ERROR_CONSTRAINT + ); + + return $MAP; +} + +function adodb_error_mssql() +{ +static $MAP = array( + 208 => DB_ERROR_NOSUCHTABLE, + 2601 => DB_ERROR_ALREADY_EXISTS + ); + + return $MAP; +} + +function adodb_error_sqlite() +{ +static $MAP = array( + 1 => DB_ERROR_SYNTAX + ); + + return $MAP; +} + +function adodb_error_mysql() +{ +static $MAP = array( + 1004 => DB_ERROR_CANNOT_CREATE, + 1005 => DB_ERROR_CANNOT_CREATE, + 1006 => DB_ERROR_CANNOT_CREATE, + 1007 => DB_ERROR_ALREADY_EXISTS, + 1008 => DB_ERROR_CANNOT_DROP, + 1045 => DB_ERROR_ACCESS_VIOLATION, + 1046 => DB_ERROR_NODBSELECTED, + 1049 => DB_ERROR_NOSUCHDB, + 1050 => DB_ERROR_ALREADY_EXISTS, + 1051 => DB_ERROR_NOSUCHTABLE, + 1054 => DB_ERROR_NOSUCHFIELD, + 1062 => DB_ERROR_ALREADY_EXISTS, + 1064 => DB_ERROR_SYNTAX, + 1100 => DB_ERROR_NOT_LOCKED, + 1136 => DB_ERROR_VALUE_COUNT_ON_ROW, + 1146 => DB_ERROR_NOSUCHTABLE, + 1048 => DB_ERROR_CONSTRAINT, + 2002 => DB_ERROR_CONNECT_FAILED, + 2005 => DB_ERROR_CONNECT_FAILED + ); + + return $MAP; +} diff --git a/app/vendor/adodb/adodb-php/adodb-errorhandler.inc.php b/app/vendor/adodb/adodb-php/adodb-errorhandler.inc.php new file mode 100644 index 000000000..7f36ba1ee --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-errorhandler.inc.php @@ -0,0 +1,80 @@ +$s

"; + trigger_error($s,ADODB_ERROR_HANDLER_TYPE); +} diff --git a/app/vendor/adodb/adodb-php/adodb-errorpear.inc.php b/app/vendor/adodb/adodb-php/adodb-errorpear.inc.php new file mode 100644 index 000000000..474d6d5ba --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-errorpear.inc.php @@ -0,0 +1,88 @@ +!$s

"; +} + +/** +* Returns last PEAR_Error object. This error might be for an error that +* occured several sql statements ago. +*/ +function ADODB_PEAR_Error() +{ +global $ADODB_Last_PEAR_Error; + + return $ADODB_Last_PEAR_Error; +} diff --git a/app/vendor/adodb/adodb-php/adodb-exceptions.inc.php b/app/vendor/adodb/adodb-php/adodb-exceptions.inc.php new file mode 100644 index 000000000..9c66ac398 --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-exceptions.inc.php @@ -0,0 +1,81 @@ +sql = is_array($p1) ? $p1[0] : $p1; + $this->params = $p2; + $s = "$dbms error: [$errno: $errmsg] in $fn(\"$this->sql\")"; + break; + + case 'PCONNECT': + case 'CONNECT': + $user = $thisConnection->user; + $s = "$dbms error: [$errno: $errmsg] in $fn($p1, '$user', '****', $p2)"; + break; + default: + $s = "$dbms error: [$errno: $errmsg] in $fn($p1, $p2)"; + break; + } + + $this->dbms = $dbms; + if ($thisConnection) { + $this->host = $thisConnection->host; + $this->database = $thisConnection->database; + } + $this->fn = $fn; + $this->msg = $errmsg; + + if (!is_numeric($errno)) $errno = -1; + parent::__construct($s,$errno); + } +} + +/** +* Default Error Handler. This will be called with the following params +* +* @param $dbms the RDBMS you are connecting to +* @param $fn the name of the calling function (in uppercase) +* @param $errno the native error number from the database +* @param $errmsg the native error msg from the database +* @param $p1 $fn specific parameter - see below +* @param $P2 $fn specific parameter - see below +*/ + +function adodb_throw($dbms, $fn, $errno, $errmsg, $p1, $p2, $thisConnection) +{ +global $ADODB_EXCEPTION; + + if (error_reporting() == 0) return; // obey @ protocol + if (is_string($ADODB_EXCEPTION)) $errfn = $ADODB_EXCEPTION; + else $errfn = 'ADODB_EXCEPTION'; + throw new $errfn($dbms, $fn, $errno, $errmsg, $p1, $p2, $thisConnection); +} diff --git a/app/vendor/adodb/adodb-php/adodb-iterator.inc.php b/app/vendor/adodb/adodb-php/adodb-iterator.inc.php new file mode 100644 index 000000000..cfc067bc8 --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-iterator.inc.php @@ -0,0 +1,26 @@ +Execute("select * from adoxyz"); + foreach($rs as $k => $v) { + echo $k; print_r($v); echo "
"; + } + + + Iterator code based on http://cvs.php.net/cvs.php/php-src/ext/spl/examples/cachingiterator.inc?login=2 + + + Moved to adodb.inc.php to improve performance. + */ diff --git a/app/vendor/adodb/adodb-php/adodb-lib.inc.php b/app/vendor/adodb/adodb-php/adodb-lib.inc.php new file mode 100644 index 000000000..60285305f --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-lib.inc.php @@ -0,0 +1,1251 @@ + sizeof($array)) $max = sizeof($array); + else $max = $probe; + + + for ($j=0;$j < $max; $j++) { + $row = $array[$j]; + if (!$row) break; + $i = -1; + foreach($row as $v) { + $i += 1; + + if (isset($types[$i]) && $types[$i]=='C') continue; + + //print " ($i ".$types[$i]. "$v) "; + $v = trim($v); + + if (!preg_match('/^[+-]{0,1}[0-9\.]+$/',$v)) { + $types[$i] = 'C'; // once C, always C + + continue; + } + if ($j == 0) { + // If empty string, we presume is character + // test for integer for 1st row only + // after that it is up to testing other rows to prove + // that it is not an integer + if (strlen($v) == 0) $types[$i] = 'C'; + if (strpos($v,'.') !== false) $types[$i] = 'N'; + else $types[$i] = 'I'; + continue; + } + + if (strpos($v,'.') !== false) $types[$i] = 'N'; + + } + } + +} + +function adodb_transpose(&$arr, &$newarr, &$hdr, &$fobjs) +{ + $oldX = sizeof(reset($arr)); + $oldY = sizeof($arr); + + if ($hdr) { + $startx = 1; + $hdr = array('Fields'); + for ($y = 0; $y < $oldY; $y++) { + $hdr[] = $arr[$y][0]; + } + } else + $startx = 0; + + for ($x = $startx; $x < $oldX; $x++) { + if ($fobjs) { + $o = $fobjs[$x]; + $newarr[] = array($o->name); + } else + $newarr[] = array(); + + for ($y = 0; $y < $oldY; $y++) { + $newarr[$x-$startx][] = $arr[$y][$x]; + } + } +} + +// Force key to upper. +// See also http://www.php.net/manual/en/function.array-change-key-case.php +function _array_change_key_case($an_array) +{ + if (is_array($an_array)) { + $new_array = array(); + foreach($an_array as $key=>$value) + $new_array[strtoupper($key)] = $value; + + return $new_array; + } + + return $an_array; +} + +function _adodb_replace(&$zthis, $table, $fieldArray, $keyCol, $autoQuote, $has_autoinc) +{ + if (count($fieldArray) == 0) return 0; + $first = true; + $uSet = ''; + + if (!is_array($keyCol)) { + $keyCol = array($keyCol); + } + foreach($fieldArray as $k => $v) { + if ($v === null) { + $v = 'NULL'; + $fieldArray[$k] = $v; + } else if ($autoQuote && /*!is_numeric($v) /*and strncmp($v,"'",1) !== 0 -- sql injection risk*/ strcasecmp($v,$zthis->null2null)!=0) { + $v = $zthis->qstr($v); + $fieldArray[$k] = $v; + } + if (in_array($k,$keyCol)) continue; // skip UPDATE if is key + + if ($first) { + $first = false; + $uSet = "$k=$v"; + } else + $uSet .= ",$k=$v"; + } + + $where = false; + foreach ($keyCol as $v) { + if (isset($fieldArray[$v])) { + if ($where) $where .= ' and '.$v.'='.$fieldArray[$v]; + else $where = $v.'='.$fieldArray[$v]; + } + } + + if ($uSet && $where) { + $update = "UPDATE $table SET $uSet WHERE $where"; + + $rs = $zthis->Execute($update); + + + if ($rs) { + if ($zthis->poorAffectedRows) { + /* + The Select count(*) wipes out any errors that the update would have returned. + http://phplens.com/lens/lensforum/msgs.php?id=5696 + */ + if ($zthis->ErrorNo()<>0) return 0; + + # affected_rows == 0 if update field values identical to old values + # for mysql - which is silly. + + $cnt = $zthis->GetOne("select count(*) from $table where $where"); + if ($cnt > 0) return 1; // record already exists + } else { + if (($zthis->Affected_Rows()>0)) return 1; + } + } else + return 0; + } + + // print "

Error=".$this->ErrorNo().'

'; + $first = true; + foreach($fieldArray as $k => $v) { + if ($has_autoinc && in_array($k,$keyCol)) continue; // skip autoinc col + + if ($first) { + $first = false; + $iCols = "$k"; + $iVals = "$v"; + } else { + $iCols .= ",$k"; + $iVals .= ",$v"; + } + } + $insert = "INSERT INTO $table ($iCols) VALUES ($iVals)"; + $rs = $zthis->Execute($insert); + return ($rs) ? 2 : 0; +} + +// Requires $ADODB_FETCH_MODE = ADODB_FETCH_NUM +function _adodb_getmenu(&$zthis, $name,$defstr='',$blank1stItem=true,$multiple=false, + $size=0, $selectAttr='',$compareFields0=true) +{ + $hasvalue = false; + + if ($multiple or is_array($defstr)) { + if ($size==0) $size=5; + $attr = ' multiple size="'.$size.'"'; + if (!strpos($name,'[]')) $name .= '[]'; + } else if ($size) $attr = ' size="'.$size.'"'; + else $attr =''; + + $s = '\n"; +} + +// Requires $ADODB_FETCH_MODE = ADODB_FETCH_NUM +function _adodb_getmenu_gp(&$zthis, $name,$defstr='',$blank1stItem=true,$multiple=false, + $size=0, $selectAttr='',$compareFields0=true) +{ + $hasvalue = false; + + if ($multiple or is_array($defstr)) { + if ($size==0) $size=5; + $attr = ' multiple size="'.$size.'"'; + if (!strpos($name,'[]')) $name .= '[]'; + } else if ($size) $attr = ' size="'.$size.'"'; + else $attr =''; + + $s = '\n"; +} + + +/* + Count the number of records this sql statement will return by using + query rewriting heuristics... + + Does not work with UNIONs, except with postgresql and oracle. + + Usage: + + $conn->Connect(...); + $cnt = _adodb_getcount($conn, $sql); + +*/ +function _adodb_getcount(&$zthis, $sql,$inputarr=false,$secs2cache=0) +{ + $qryRecs = 0; + + if (!empty($zthis->_nestedSQL) || preg_match("/^\s*SELECT\s+DISTINCT/is", $sql) || + preg_match('/\s+GROUP\s+BY\s+/is',$sql) || + preg_match('/\s+UNION\s+/is',$sql)) { + + $rewritesql = adodb_strip_order_by($sql); + + // ok, has SELECT DISTINCT or GROUP BY so see if we can use a table alias + // but this is only supported by oracle and postgresql... + if ($zthis->dataProvider == 'oci8') { + // Allow Oracle hints to be used for query optimization, Chris Wrye + if (preg_match('#/\\*+.*?\\*\\/#', $sql, $hint)) { + $rewritesql = "SELECT ".$hint[0]." COUNT(*) FROM (".$rewritesql.")"; + } else + $rewritesql = "SELECT COUNT(*) FROM (".$rewritesql.")"; + + } else if (strncmp($zthis->databaseType,'postgres',8) == 0 || strncmp($zthis->databaseType,'mysql',5) == 0) { + $rewritesql = "SELECT COUNT(*) FROM ($rewritesql) _ADODB_ALIAS_"; + } else { + $rewritesql = "SELECT COUNT(*) FROM ($rewritesql)"; + } + } else { + // now replace SELECT ... FROM with SELECT COUNT(*) FROM + if ( strpos($sql, '_ADODB_COUNT') !== FALSE ) { + $rewritesql = preg_replace('/^\s*?SELECT\s+_ADODB_COUNT(.*)_ADODB_COUNT\s/is','SELECT COUNT(*) ',$sql); + } else { + $rewritesql = preg_replace('/^\s*SELECT\s.*\s+FROM\s/Uis','SELECT COUNT(*) FROM ',$sql); + } + // fix by alexander zhukov, alex#unipack.ru, because count(*) and 'order by' fails + // with mssql, access and postgresql. Also a good speedup optimization - skips sorting! + // also see http://phplens.com/lens/lensforum/msgs.php?id=12752 + $rewritesql = adodb_strip_order_by($rewritesql); + } + + if (isset($rewritesql) && $rewritesql != $sql) { + if (preg_match('/\sLIMIT\s+[0-9]+/i',$sql,$limitarr)) $rewritesql .= $limitarr[0]; + + if ($secs2cache) { + // we only use half the time of secs2cache because the count can quickly + // become inaccurate if new records are added + $qryRecs = $zthis->CacheGetOne($secs2cache/2,$rewritesql,$inputarr); + + } else { + $qryRecs = $zthis->GetOne($rewritesql,$inputarr); + } + if ($qryRecs !== false) return $qryRecs; + } + //-------------------------------------------- + // query rewrite failed - so try slower way... + + + // strip off unneeded ORDER BY if no UNION + if (preg_match('/\s*UNION\s*/is', $sql)) $rewritesql = $sql; + else $rewritesql = $rewritesql = adodb_strip_order_by($sql); + + if (preg_match('/\sLIMIT\s+[0-9]+/i',$sql,$limitarr)) $rewritesql .= $limitarr[0]; + + if ($secs2cache) { + $rstest = $zthis->CacheExecute($secs2cache,$rewritesql,$inputarr); + if (!$rstest) $rstest = $zthis->CacheExecute($secs2cache,$sql,$inputarr); + } else { + $rstest = $zthis->Execute($rewritesql,$inputarr); + if (!$rstest) $rstest = $zthis->Execute($sql,$inputarr); + } + if ($rstest) { + $qryRecs = $rstest->RecordCount(); + if ($qryRecs == -1) { + global $ADODB_EXTENSION; + // some databases will return -1 on MoveLast() - change to MoveNext() + if ($ADODB_EXTENSION) { + while(!$rstest->EOF) { + adodb_movenext($rstest); + } + } else { + while(!$rstest->EOF) { + $rstest->MoveNext(); + } + } + $qryRecs = $rstest->_currentRow; + } + $rstest->Close(); + if ($qryRecs == -1) return 0; + } + return $qryRecs; +} + +/* + Code originally from "Cornel G" + + This code might not work with SQL that has UNION in it + + Also if you are using CachePageExecute(), there is a strong possibility that + data will get out of synch. use CachePageExecute() only with tables that + rarely change. +*/ +function _adodb_pageexecute_all_rows(&$zthis, $sql, $nrows, $page, + $inputarr=false, $secs2cache=0) +{ + $atfirstpage = false; + $atlastpage = false; + $lastpageno=1; + + // If an invalid nrows is supplied, + // we assume a default value of 10 rows per page + if (!isset($nrows) || $nrows <= 0) $nrows = 10; + + $qryRecs = false; //count records for no offset + + $qryRecs = _adodb_getcount($zthis,$sql,$inputarr,$secs2cache); + $lastpageno = (int) ceil($qryRecs / $nrows); + $zthis->_maxRecordCount = $qryRecs; + + + + // ***** Here we check whether $page is the last page or + // whether we are trying to retrieve + // a page number greater than the last page number. + if ($page >= $lastpageno) { + $page = $lastpageno; + $atlastpage = true; + } + + // If page number <= 1, then we are at the first page + if (empty($page) || $page <= 1) { + $page = 1; + $atfirstpage = true; + } + + // We get the data we want + $offset = $nrows * ($page-1); + if ($secs2cache > 0) + $rsreturn = $zthis->CacheSelectLimit($secs2cache, $sql, $nrows, $offset, $inputarr); + else + $rsreturn = $zthis->SelectLimit($sql, $nrows, $offset, $inputarr, $secs2cache); + + + // Before returning the RecordSet, we set the pagination properties we need + if ($rsreturn) { + $rsreturn->_maxRecordCount = $qryRecs; + $rsreturn->rowsPerPage = $nrows; + $rsreturn->AbsolutePage($page); + $rsreturn->AtFirstPage($atfirstpage); + $rsreturn->AtLastPage($atlastpage); + $rsreturn->LastPageNo($lastpageno); + } + return $rsreturn; +} + +// Iván Oliva version +function _adodb_pageexecute_no_last_page(&$zthis, $sql, $nrows, $page, $inputarr=false, $secs2cache=0) +{ + + $atfirstpage = false; + $atlastpage = false; + + if (!isset($page) || $page <= 1) { + // If page number <= 1, then we are at the first page + $page = 1; + $atfirstpage = true; + } + if ($nrows <= 0) { + // If an invalid nrows is supplied, we assume a default value of 10 rows per page + $nrows = 10; + } + + $pagecounteroffset = ($page * $nrows) - $nrows; + + // To find out if there are more pages of rows, simply increase the limit or + // nrows by 1 and see if that number of records was returned. If it was, + // then we know there is at least one more page left, otherwise we are on + // the last page. Therefore allow non-Count() paging with single queries + // rather than three queries as was done before. + $test_nrows = $nrows + 1; + if ($secs2cache > 0) { + $rsreturn = $zthis->CacheSelectLimit($secs2cache, $sql, $nrows, $pagecounteroffset, $inputarr); + } else { + $rsreturn = $zthis->SelectLimit($sql, $test_nrows, $pagecounteroffset, $inputarr, $secs2cache); + } + + // Now check to see if the number of rows returned was the higher value we asked for or not. + if ( $rsreturn->_numOfRows == $test_nrows ) { + // Still at least 1 more row, so we are not on last page yet... + // Remove the last row from the RS. + $rsreturn->_numOfRows = ( $rsreturn->_numOfRows - 1 ); + } elseif ( $rsreturn->_numOfRows == 0 && $page > 1 ) { + // Likely requested a page that doesn't exist, so need to find the last + // page and return it. Revert to original method and loop through pages + // until we find some data... + $pagecounter = $page + 1; + $pagecounteroffset = ($pagecounter * $nrows) - $nrows; + + $rstest = $rsreturn; + if ($rstest) { + while ($rstest && $rstest->EOF && $pagecounter > 0) { + $atlastpage = true; + $pagecounter--; + $pagecounteroffset = $nrows * ($pagecounter - 1); + $rstest->Close(); + if ($secs2cache>0) { + $rstest = $zthis->CacheSelectLimit($secs2cache, $sql, $nrows, $pagecounteroffset, $inputarr); + } + else { + $rstest = $zthis->SelectLimit($sql, $nrows, $pagecounteroffset, $inputarr, $secs2cache); + } + } + if ($rstest) $rstest->Close(); + } + if ($atlastpage) { + // If we are at the last page or beyond it, we are going to retrieve it + $page = $pagecounter; + if ($page == 1) { + // We have to do this again in case the last page is the same as + // the first page, that is, the recordset has only 1 page. + $atfirstpage = true; + } + } + // We get the data we want + $offset = $nrows * ($page-1); + if ($secs2cache > 0) { + $rsreturn = $zthis->CacheSelectLimit($secs2cache, $sql, $nrows, $offset, $inputarr); + } + else { + $rsreturn = $zthis->SelectLimit($sql, $nrows, $offset, $inputarr, $secs2cache); + } + } elseif ( $rsreturn->_numOfRows < $test_nrows ) { + // Rows is less than what we asked for, so must be at the last page. + $atlastpage = true; + } + + // Before returning the RecordSet, we set the pagination properties we need + if ($rsreturn) { + $rsreturn->rowsPerPage = $nrows; + $rsreturn->AbsolutePage($page); + $rsreturn->AtFirstPage($atfirstpage); + $rsreturn->AtLastPage($atlastpage); + } + return $rsreturn; +} + +function _adodb_getupdatesql(&$zthis,&$rs, $arrFields,$forceUpdate=false,$magicq=false,$force=2) +{ + global $ADODB_QUOTE_FIELDNAMES; + + if (!$rs) { + printf(ADODB_BAD_RS,'GetUpdateSQL'); + return false; + } + + $fieldUpdatedCount = 0; + $arrFields = _array_change_key_case($arrFields); + + $hasnumeric = isset($rs->fields[0]); + $setFields = ''; + + // Loop through all of the fields in the recordset + for ($i=0, $max=$rs->FieldCount(); $i < $max; $i++) { + // Get the field from the recordset + $field = $rs->FetchField($i); + + // If the recordset field is one + // of the fields passed in then process. + $upperfname = strtoupper($field->name); + if (adodb_key_exists($upperfname,$arrFields,$force)) { + + // If the existing field value in the recordset + // is different from the value passed in then + // go ahead and append the field name and new value to + // the update query. + + if ($hasnumeric) $val = $rs->fields[$i]; + else if (isset($rs->fields[$upperfname])) $val = $rs->fields[$upperfname]; + else if (isset($rs->fields[$field->name])) $val = $rs->fields[$field->name]; + else if (isset($rs->fields[strtolower($upperfname)])) $val = $rs->fields[strtolower($upperfname)]; + else $val = ''; + + + if ($forceUpdate || strcmp($val, $arrFields[$upperfname])) { + // Set the counter for the number of fields that will be updated. + $fieldUpdatedCount++; + + // Based on the datatype of the field + // Format the value properly for the database + $type = $rs->MetaType($field->type); + + + if ($type == 'null') { + $type = 'C'; + } + + if ((strpos($upperfname,' ') !== false) || ($ADODB_QUOTE_FIELDNAMES)) { + switch ($ADODB_QUOTE_FIELDNAMES) { + case 'LOWER': + $fnameq = $zthis->nameQuote.strtolower($field->name).$zthis->nameQuote;break; + case 'NATIVE': + $fnameq = $zthis->nameQuote.$field->name.$zthis->nameQuote;break; + case 'UPPER': + default: + $fnameq = $zthis->nameQuote.$upperfname.$zthis->nameQuote;break; + } + } else + $fnameq = $upperfname; + + //********************************************************// + if (is_null($arrFields[$upperfname]) + || (empty($arrFields[$upperfname]) && strlen($arrFields[$upperfname]) == 0) + || $arrFields[$upperfname] === $zthis->null2null + ) + { + switch ($force) { + + //case 0: + // //Ignore empty values. This is allready handled in "adodb_key_exists" function. + //break; + + case 1: + //Set null + $setFields .= $field->name . " = null, "; + break; + + case 2: + //Set empty + $arrFields[$upperfname] = ""; + $setFields .= _adodb_column_sql($zthis, 'U', $type, $upperfname, $fnameq,$arrFields, $magicq); + break; + default: + case 3: + //Set the value that was given in array, so you can give both null and empty values + if (is_null($arrFields[$upperfname]) || $arrFields[$upperfname] === $zthis->null2null) { + $setFields .= $field->name . " = null, "; + } else { + $setFields .= _adodb_column_sql($zthis, 'U', $type, $upperfname, $fnameq,$arrFields, $magicq); + } + break; + } + //********************************************************// + } else { + //we do this so each driver can customize the sql for + //DB specific column types. + //Oracle needs BLOB types to be handled with a returning clause + //postgres has special needs as well + $setFields .= _adodb_column_sql($zthis, 'U', $type, $upperfname, $fnameq, + $arrFields, $magicq); + } + } + } + } + + // If there were any modified fields then build the rest of the update query. + if ($fieldUpdatedCount > 0 || $forceUpdate) { + // Get the table name from the existing query. + if (!empty($rs->tableName)) $tableName = $rs->tableName; + else { + preg_match("/FROM\s+".ADODB_TABLE_REGEX."/is", $rs->sql, $tableName); + $tableName = $tableName[1]; + } + // Get the full where clause excluding the word "WHERE" from + // the existing query. + preg_match('/\sWHERE\s(.*)/is', $rs->sql, $whereClause); + + $discard = false; + // not a good hack, improvements? + if ($whereClause) { + #var_dump($whereClause); + if (preg_match('/\s(ORDER\s.*)/is', $whereClause[1], $discard)); + else if (preg_match('/\s(LIMIT\s.*)/is', $whereClause[1], $discard)); + else if (preg_match('/\s(FOR UPDATE.*)/is', $whereClause[1], $discard)); + else preg_match('/\s.*(\) WHERE .*)/is', $whereClause[1], $discard); # see http://sourceforge.net/tracker/index.php?func=detail&aid=1379638&group_id=42718&atid=433976 + } else + $whereClause = array(false,false); + + if ($discard) + $whereClause[1] = substr($whereClause[1], 0, strlen($whereClause[1]) - strlen($discard[1])); + + $sql = 'UPDATE '.$tableName.' SET '.substr($setFields, 0, -2); + if (strlen($whereClause[1]) > 0) + $sql .= ' WHERE '.$whereClause[1]; + + return $sql; + + } else { + return false; + } +} + +function adodb_key_exists($key, &$arr,$force=2) +{ + if ($force<=0) { + // the following is the old behaviour where null or empty fields are ignored + return (!empty($arr[$key])) || (isset($arr[$key]) && strlen($arr[$key])>0); + } + + if (isset($arr[$key])) return true; + ## null check below + if (ADODB_PHPVER >= 0x4010) return array_key_exists($key,$arr); + return false; +} + +/** + * There is a special case of this function for the oci8 driver. + * The proper way to handle an insert w/ a blob in oracle requires + * a returning clause with bind variables and a descriptor blob. + * + * + */ +function _adodb_getinsertsql(&$zthis,&$rs,$arrFields,$magicq=false,$force=2) +{ +static $cacheRS = false; +static $cacheSig = 0; +static $cacheCols; + global $ADODB_QUOTE_FIELDNAMES; + + $tableName = ''; + $values = ''; + $fields = ''; + $recordSet = null; + $arrFields = _array_change_key_case($arrFields); + $fieldInsertedCount = 0; + + if (is_string($rs)) { + //ok we have a table name + //try and get the column info ourself. + $tableName = $rs; + + //we need an object for the recordSet + //because we have to call MetaType. + //php can't do a $rsclass::MetaType() + $rsclass = $zthis->rsPrefix.$zthis->databaseType; + $recordSet = new $rsclass(-1,$zthis->fetchMode); + $recordSet->connection = $zthis; + + if (is_string($cacheRS) && $cacheRS == $rs) { + $columns = $cacheCols; + } else { + $columns = $zthis->MetaColumns( $tableName ); + $cacheRS = $tableName; + $cacheCols = $columns; + } + } else if (is_subclass_of($rs, 'adorecordset')) { + if (isset($rs->insertSig) && is_integer($cacheRS) && $cacheRS == $rs->insertSig) { + $columns = $cacheCols; + } else { + for ($i=0, $max=$rs->FieldCount(); $i < $max; $i++) + $columns[] = $rs->FetchField($i); + $cacheRS = $cacheSig; + $cacheCols = $columns; + $rs->insertSig = $cacheSig++; + } + $recordSet = $rs; + + } else { + printf(ADODB_BAD_RS,'GetInsertSQL'); + return false; + } + + // Loop through all of the fields in the recordset + foreach( $columns as $field ) { + $upperfname = strtoupper($field->name); + if (adodb_key_exists($upperfname,$arrFields,$force)) { + $bad = false; + if ((strpos($upperfname,' ') !== false) || ($ADODB_QUOTE_FIELDNAMES)) { + switch ($ADODB_QUOTE_FIELDNAMES) { + case 'LOWER': + $fnameq = $zthis->nameQuote.strtolower($field->name).$zthis->nameQuote;break; + case 'NATIVE': + $fnameq = $zthis->nameQuote.$field->name.$zthis->nameQuote;break; + case 'UPPER': + default: + $fnameq = $zthis->nameQuote.$upperfname.$zthis->nameQuote;break; + } + } else + $fnameq = $upperfname; + + $type = $recordSet->MetaType($field->type); + + /********************************************************/ + if (is_null($arrFields[$upperfname]) + || (empty($arrFields[$upperfname]) && strlen($arrFields[$upperfname]) == 0) + || $arrFields[$upperfname] === $zthis->null2null + ) + { + switch ($force) { + + case 0: // we must always set null if missing + $bad = true; + break; + + case 1: + $values .= "null, "; + break; + + case 2: + //Set empty + $arrFields[$upperfname] = ""; + $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq,$arrFields, $magicq); + break; + + default: + case 3: + //Set the value that was given in array, so you can give both null and empty values + if (is_null($arrFields[$upperfname]) || $arrFields[$upperfname] === $zthis->null2null) { + $values .= "null, "; + } else { + $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq, $arrFields, $magicq); + } + break; + } // switch + + /*********************************************************/ + } else { + //we do this so each driver can customize the sql for + //DB specific column types. + //Oracle needs BLOB types to be handled with a returning clause + //postgres has special needs as well + $values .= _adodb_column_sql($zthis, 'I', $type, $upperfname, $fnameq, + $arrFields, $magicq); + } + + if ($bad) continue; + // Set the counter for the number of fields that will be inserted. + $fieldInsertedCount++; + + + // Get the name of the fields to insert + $fields .= $fnameq . ", "; + } + } + + + // If there were any inserted fields then build the rest of the insert query. + if ($fieldInsertedCount <= 0) return false; + + // Get the table name from the existing query. + if (!$tableName) { + if (!empty($rs->tableName)) $tableName = $rs->tableName; + else if (preg_match("/FROM\s+".ADODB_TABLE_REGEX."/is", $rs->sql, $tableName)) + $tableName = $tableName[1]; + else + return false; + } + + // Strip off the comma and space on the end of both the fields + // and their values. + $fields = substr($fields, 0, -2); + $values = substr($values, 0, -2); + + // Append the fields and their values to the insert query. + return 'INSERT INTO '.$tableName.' ( '.$fields.' ) VALUES ( '.$values.' )'; +} + + +/** + * This private method is used to help construct + * the update/sql which is generated by GetInsertSQL and GetUpdateSQL. + * It handles the string construction of 1 column -> sql string based on + * the column type. We want to do 'safe' handling of BLOBs + * + * @param string the type of sql we are trying to create + * 'I' or 'U'. + * @param string column data type from the db::MetaType() method + * @param string the column name + * @param array the column value + * + * @return string + * + */ +function _adodb_column_sql_oci8(&$zthis,$action, $type, $fname, $fnameq, $arrFields, $magicq) +{ + $sql = ''; + + // Based on the datatype of the field + // Format the value properly for the database + switch($type) { + case 'B': + //in order to handle Blobs correctly, we need + //to do some magic for Oracle + + //we need to create a new descriptor to handle + //this properly + if (!empty($zthis->hasReturningInto)) { + if ($action == 'I') { + $sql = 'empty_blob(), '; + } else { + $sql = $fnameq. '=empty_blob(), '; + } + //add the variable to the returning clause array + //so the user can build this later in + //case they want to add more to it + $zthis->_returningArray[$fname] = ':xx'.$fname.'xx'; + } else if (empty($arrFields[$fname])){ + if ($action == 'I') { + $sql = 'empty_blob(), '; + } else { + $sql = $fnameq. '=empty_blob(), '; + } + } else { + //this is to maintain compatibility + //with older adodb versions. + $sql = _adodb_column_sql($zthis, $action, $type, $fname, $fnameq, $arrFields, $magicq,false); + } + break; + + case "X": + //we need to do some more magic here for long variables + //to handle these correctly in oracle. + + //create a safe bind var name + //to avoid conflicts w/ dupes. + if (!empty($zthis->hasReturningInto)) { + if ($action == 'I') { + $sql = ':xx'.$fname.'xx, '; + } else { + $sql = $fnameq.'=:xx'.$fname.'xx, '; + } + //add the variable to the returning clause array + //so the user can build this later in + //case they want to add more to it + $zthis->_returningArray[$fname] = ':xx'.$fname.'xx'; + } else { + //this is to maintain compatibility + //with older adodb versions. + $sql = _adodb_column_sql($zthis, $action, $type, $fname, $fnameq, $arrFields, $magicq,false); + } + break; + + default: + $sql = _adodb_column_sql($zthis, $action, $type, $fname, $fnameq, $arrFields, $magicq,false); + break; + } + + return $sql; +} + +function _adodb_column_sql(&$zthis, $action, $type, $fname, $fnameq, $arrFields, $magicq, $recurse=true) +{ + + if ($recurse) { + switch($zthis->dataProvider) { + case 'postgres': + if ($type == 'L') $type = 'C'; + break; + case 'oci8': + return _adodb_column_sql_oci8($zthis, $action, $type, $fname, $fnameq, $arrFields, $magicq); + + } + } + + switch($type) { + case "C": + case "X": + case 'B': + $val = $zthis->qstr($arrFields[$fname],$magicq); + break; + + case "D": + $val = $zthis->DBDate($arrFields[$fname]); + break; + + case "T": + $val = $zthis->DBTimeStamp($arrFields[$fname]); + break; + + case "N": + $val = $arrFields[$fname]; + if (!is_numeric($val)) $val = str_replace(',', '.', (float)$val); + break; + + case "I": + case "R": + $val = $arrFields[$fname]; + if (!is_numeric($val)) $val = (integer) $val; + break; + + default: + $val = str_replace(array("'"," ","("),"",$arrFields[$fname]); // basic sql injection defence + if (empty($val)) $val = '0'; + break; + } + + if ($action == 'I') return $val . ", "; + + + return $fnameq . "=" . $val . ", "; + +} + + + +function _adodb_debug_execute(&$zthis, $sql, $inputarr) +{ + $ss = ''; + if ($inputarr) { + foreach($inputarr as $kk=>$vv) { + if (is_string($vv) && strlen($vv)>64) $vv = substr($vv,0,64).'...'; + if (is_null($vv)) $ss .= "($kk=>null) "; + else $ss .= "($kk=>'$vv') "; + } + $ss = "[ $ss ]"; + } + $sqlTxt = is_array($sql) ? $sql[0] : $sql; + /*str_replace(', ','##1#__^LF',is_array($sql) ? $sql[0] : $sql); + $sqlTxt = str_replace(',',', ',$sqlTxt); + $sqlTxt = str_replace('##1#__^LF', ', ' ,$sqlTxt); + */ + // check if running from browser or command-line + $inBrowser = isset($_SERVER['HTTP_USER_AGENT']); + + $dbt = $zthis->databaseType; + if (isset($zthis->dsnType)) $dbt .= '-'.$zthis->dsnType; + if ($inBrowser) { + if ($ss) { + $ss = ''.htmlspecialchars($ss).''; + } + if ($zthis->debug === -1) + ADOConnection::outp( "
\n($dbt): ".htmlspecialchars($sqlTxt)."   $ss\n
\n",false); + else if ($zthis->debug !== -99) + ADOConnection::outp( "


\n($dbt): ".htmlspecialchars($sqlTxt)."   $ss\n
\n",false); + } else { + $ss = "\n ".$ss; + if ($zthis->debug !== -99) + ADOConnection::outp("-----
\n($dbt): ".$sqlTxt." $ss\n-----
\n",false); + } + + $qID = $zthis->_query($sql,$inputarr); + + /* + Alexios Fakios notes that ErrorMsg() must be called before ErrorNo() for mssql + because ErrorNo() calls Execute('SELECT @ERROR'), causing recursion + */ + if ($zthis->databaseType == 'mssql') { + // ErrorNo is a slow function call in mssql, and not reliable in PHP 4.0.6 + + if($emsg = $zthis->ErrorMsg()) { + if ($err = $zthis->ErrorNo()) { + if ($zthis->debug === -99) + ADOConnection::outp( "
\n($dbt): ".htmlspecialchars($sqlTxt)."   $ss\n
\n",false); + + ADOConnection::outp($err.': '.$emsg); + } + } + } else if (!$qID) { + + if ($zthis->debug === -99) + if ($inBrowser) ADOConnection::outp( "
\n($dbt): ".htmlspecialchars($sqlTxt)."   $ss\n
\n",false); + else ADOConnection::outp("-----
\n($dbt): ".$sqlTxt."$ss\n-----
\n",false); + + ADOConnection::outp($zthis->ErrorNo() .': '. $zthis->ErrorMsg()); + } + + if ($zthis->debug === 99) _adodb_backtrace(true,9999,2); + return $qID; +} + +# pretty print the debug_backtrace function +function _adodb_backtrace($printOrArr=true,$levels=9999,$skippy=0,$ishtml=null) +{ + if (!function_exists('debug_backtrace')) return ''; + + if ($ishtml === null) $html = (isset($_SERVER['HTTP_USER_AGENT'])); + else $html = $ishtml; + + $fmt = ($html) ? " %% line %4d, file: %s" : "%% line %4d, file: %s"; + + $MAXSTRLEN = 128; + + $s = ($html) ? '
' : '';
+
+	if (is_array($printOrArr)) $traceArr = $printOrArr;
+	else $traceArr = debug_backtrace();
+	array_shift($traceArr);
+	array_shift($traceArr);
+	$tabs = sizeof($traceArr)-2;
+
+	foreach ($traceArr as $arr) {
+		if ($skippy) {$skippy -= 1; continue;}
+		$levels -= 1;
+		if ($levels < 0) break;
+
+		$args = array();
+		for ($i=0; $i < $tabs; $i++) $s .=  ($html) ? '   ' : "\t";
+		$tabs -= 1;
+		if ($html) $s .= '';
+		if (isset($arr['class'])) $s .= $arr['class'].'.';
+		if (isset($arr['args']))
+		 foreach($arr['args'] as $v) {
+			if (is_null($v)) $args[] = 'null';
+			else if (is_array($v)) $args[] = 'Array['.sizeof($v).']';
+			else if (is_object($v)) $args[] = 'Object:'.get_class($v);
+			else if (is_bool($v)) $args[] = $v ? 'true' : 'false';
+			else {
+				$v = (string) @$v;
+				$str = htmlspecialchars(str_replace(array("\r","\n"),' ',substr($v,0,$MAXSTRLEN)));
+				if (strlen($v) > $MAXSTRLEN) $str .= '...';
+				$args[] = $str;
+			}
+		}
+		$s .= $arr['function'].'('.implode(', ',$args).')';
+
+
+		$s .= @sprintf($fmt, $arr['line'],$arr['file'],basename($arr['file']));
+
+		$s .= "\n";
+	}
+	if ($html) $s .= '
'; + if ($printOrArr) print $s; + + return $s; +} +/* +function _adodb_find_from($sql) +{ + + $sql = str_replace(array("\n","\r"), ' ', $sql); + $charCount = strlen($sql); + + $inString = false; + $quote = ''; + $parentheseCount = 0; + $prevChars = ''; + $nextChars = ''; + + + for($i = 0; $i < $charCount; $i++) { + + $char = substr($sql,$i,1); + $prevChars = substr($sql,0,$i); + $nextChars = substr($sql,$i+1); + + if((($char == "'" || $char == '"' || $char == '`') && substr($prevChars,-1,1) != '\\') && $inString === false) { + $quote = $char; + $inString = true; + } + + elseif((($char == "'" || $char == '"' || $char == '`') && substr($prevChars,-1,1) != '\\') && $inString === true && $quote == $char) { + $quote = ""; + $inString = false; + } + + elseif($char == "(" && $inString === false) + $parentheseCount++; + + elseif($char == ")" && $inString === false && $parentheseCount > 0) + $parentheseCount--; + + elseif($parentheseCount <= 0 && $inString === false && $char == " " && strtoupper(substr($prevChars,-5,5)) == " FROM") + return $i; + + } +} +*/ diff --git a/app/vendor/adodb/adodb-php/adodb-memcache.lib.inc.php b/app/vendor/adodb/adodb-php/adodb-memcache.lib.inc.php new file mode 100644 index 000000000..42d2be62e --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-memcache.lib.inc.php @@ -0,0 +1,190 @@ +memCache = true; /// should we use memCache instead of caching in files +$db->memCacheHost = array($ip1, $ip2, $ip3); +$db->memCachePort = 11211; /// this is default memCache port +$db->memCacheCompress = false; /// Use 'true' to store the item compressed (uses zlib) + +$db->Connect(...); +$db->CacheExecute($sql); + + Note the memcache class is shared by all connections, is created during the first call to Connect/PConnect. + + Class instance is stored in $ADODB_CACHE +*/ + + class ADODB_Cache_MemCache { + var $createdir = false; // create caching directory structure? + + //----------------------------- + // memcache specific variables + + var $hosts; // array of hosts + var $port = 11211; + var $compress = false; // memcache compression with zlib + + var $_connected = false; + var $_memcache = false; + + function __construct(&$obj) + { + $this->hosts = $obj->memCacheHost; + $this->port = $obj->memCachePort; + $this->compress = $obj->memCacheCompress; + } + + // implement as lazy connection. The connection only occurs on CacheExecute call + function connect(&$err) + { + if (!function_exists('memcache_pconnect')) { + $err = 'Memcache module PECL extension not found!'; + return false; + } + + $memcache = new MemCache; + + if (!is_array($this->hosts)) $this->hosts = array($this->hosts); + + $failcnt = 0; + foreach($this->hosts as $host) { + if (!@$memcache->addServer($host,$this->port,true)) { + $failcnt += 1; + } + } + if ($failcnt == sizeof($this->hosts)) { + $err = 'Can\'t connect to any memcache server'; + return false; + } + $this->_connected = true; + $this->_memcache = $memcache; + return true; + } + + // returns true or false. true if successful save + function writecache($filename, $contents, $debug, $secs2cache) + { + if (!$this->_connected) { + $err = ''; + if (!$this->connect($err) && $debug) ADOConnection::outp($err); + } + if (!$this->_memcache) return false; + + if (!$this->_memcache->set($filename, $contents, $this->compress ? MEMCACHE_COMPRESSED : 0, $secs2cache)) { + if ($debug) ADOConnection::outp(" Failed to save data at the memcached server!
\n"); + return false; + } + + return true; + } + + // returns a recordset + function readcache($filename, &$err, $secs2cache, $rsClass) + { + $false = false; + if (!$this->_connected) $this->connect($err); + if (!$this->_memcache) return $false; + + $rs = $this->_memcache->get($filename); + if (!$rs) { + $err = 'Item with such key doesn\'t exists on the memcached server.'; + return $false; + } + + // hack, should actually use _csv2rs + $rs = explode("\n", $rs); + unset($rs[0]); + $rs = join("\n", $rs); + $rs = unserialize($rs); + if (! is_object($rs)) { + $err = 'Unable to unserialize $rs'; + return $false; + } + if ($rs->timeCreated == 0) return $rs; // apparently have been reports that timeCreated was set to 0 somewhere + + $tdiff = intval($rs->timeCreated+$secs2cache - time()); + if ($tdiff <= 2) { + switch($tdiff) { + case 2: + if ((rand() & 15) == 0) { + $err = "Timeout 2"; + return $false; + } + break; + case 1: + if ((rand() & 3) == 0) { + $err = "Timeout 1"; + return $false; + } + break; + default: + $err = "Timeout 0"; + return $false; + } + } + return $rs; + } + + function flushall($debug=false) + { + if (!$this->_connected) { + $err = ''; + if (!$this->connect($err) && $debug) ADOConnection::outp($err); + } + if (!$this->_memcache) return false; + + $del = $this->_memcache->flush(); + + if ($debug) + if (!$del) ADOConnection::outp("flushall: failed!
\n"); + else ADOConnection::outp("flushall: succeeded!
\n"); + + return $del; + } + + function flushcache($filename, $debug=false) + { + if (!$this->_connected) { + $err = ''; + if (!$this->connect($err) && $debug) ADOConnection::outp($err); + } + if (!$this->_memcache) return false; + + $del = $this->_memcache->delete($filename); + + if ($debug) + if (!$del) ADOConnection::outp("flushcache: $key entry doesn't exist on memcached server!
\n"); + else ADOConnection::outp("flushcache: $key entry flushed from memcached server!
\n"); + + return $del; + } + + // not used for memcache + function createdir($dir, $hash) + { + return true; + } + } diff --git a/app/vendor/adodb/adodb-php/adodb-pager.inc.php b/app/vendor/adodb/adodb-php/adodb-pager.inc.php new file mode 100644 index 000000000..fa77d55c7 --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-pager.inc.php @@ -0,0 +1,289 @@ + implemented Render_PageLinks(). + + Please note, this class is entirely unsupported, + and no free support requests except for bug reports + will be entertained by the author. + +*/ +class ADODB_Pager { + var $id; // unique id for pager (defaults to 'adodb') + var $db; // ADODB connection object + var $sql; // sql used + var $rs; // recordset generated + var $curr_page; // current page number before Render() called, calculated in constructor + var $rows; // number of rows per page + var $linksPerPage=10; // number of links per page in navigation bar + var $showPageLinks; + + var $gridAttributes = 'width=100% border=1 bgcolor=white'; + + // Localize text strings here + var $first = '|<'; + var $prev = '<<'; + var $next = '>>'; + var $last = '>|'; + var $moreLinks = '...'; + var $startLinks = '...'; + var $gridHeader = false; + var $htmlSpecialChars = true; + var $page = 'Page'; + var $linkSelectedColor = 'red'; + var $cache = 0; #secs to cache with CachePageExecute() + + //---------------------------------------------- + // constructor + // + // $db adodb connection object + // $sql sql statement + // $id optional id to identify which pager, + // if you have multiple on 1 page. + // $id should be only be [a-z0-9]* + // + function __construct(&$db,$sql,$id = 'adodb', $showPageLinks = false) + { + global $PHP_SELF; + + $curr_page = $id.'_curr_page'; + if (!empty($PHP_SELF)) $PHP_SELF = htmlspecialchars($_SERVER['PHP_SELF']); // htmlspecialchars() to prevent XSS attacks + + $this->sql = $sql; + $this->id = $id; + $this->db = $db; + $this->showPageLinks = $showPageLinks; + + $next_page = $id.'_next_page'; + + if (isset($_GET[$next_page])) { + $_SESSION[$curr_page] = (integer) $_GET[$next_page]; + } + if (empty($_SESSION[$curr_page])) $_SESSION[$curr_page] = 1; ## at first page + + $this->curr_page = $_SESSION[$curr_page]; + + } + + //--------------------------- + // Display link to first page + function Render_First($anchor=true) + { + global $PHP_SELF; + if ($anchor) { + ?> + first;?>   + first   "; + } + } + + //-------------------------- + // Display link to next page + function render_next($anchor=true) + { + global $PHP_SELF; + + if ($anchor) { + ?> + next;?>   + next   "; + } + } + + //------------------ + // Link to last page + // + // for better performance with large recordsets, you can set + // $this->db->pageExecuteCountRows = false, which disables + // last page counting. + function render_last($anchor=true) + { + global $PHP_SELF; + + if (!$this->db->pageExecuteCountRows) return; + + if ($anchor) { + ?> + last;?>   + last   "; + } + } + + //--------------------------------------------------- + // original code by "Pablo Costa" + function render_pagelinks() + { + global $PHP_SELF; + $pages = $this->rs->LastPageNo(); + $linksperpage = $this->linksPerPage ? $this->linksPerPage : $pages; + for($i=1; $i <= $pages; $i+=$linksperpage) + { + if($this->rs->AbsolutePage() >= $i) + { + $start = $i; + } + } + $numbers = ''; + $end = $start+$linksperpage-1; + $link = $this->id . "_next_page"; + if($end > $pages) $end = $pages; + + + if ($this->startLinks && $start > 1) { + $pos = $start - 1; + $numbers .= "$this->startLinks "; + } + + for($i=$start; $i <= $end; $i++) { + if ($this->rs->AbsolutePage() == $i) + $numbers .= "linkSelectedColor>$i "; + else + $numbers .= "$i "; + + } + if ($this->moreLinks && $end < $pages) + $numbers .= "$this->moreLinks "; + print $numbers . '   '; + } + // Link to previous page + function render_prev($anchor=true) + { + global $PHP_SELF; + if ($anchor) { + ?> + prev;?>   + prev   "; + } + } + + //-------------------------------------------------------- + // Simply rendering of grid. You should override this for + // better control over the format of the grid + // + // We use output buffering to keep code clean and readable. + function RenderGrid() + { + global $gSQLBlockRows; // used by rs2html to indicate how many rows to display + include_once(ADODB_DIR.'/tohtml.inc.php'); + ob_start(); + $gSQLBlockRows = $this->rows; + rs2html($this->rs,$this->gridAttributes,$this->gridHeader,$this->htmlSpecialChars); + $s = ob_get_contents(); + ob_end_clean(); + return $s; + } + + //------------------------------------------------------- + // Navigation bar + // + // we use output buffering to keep the code easy to read. + function RenderNav() + { + ob_start(); + if (!$this->rs->AtFirstPage()) { + $this->Render_First(); + $this->Render_Prev(); + } else { + $this->Render_First(false); + $this->Render_Prev(false); + } + if ($this->showPageLinks){ + $this->Render_PageLinks(); + } + if (!$this->rs->AtLastPage()) { + $this->Render_Next(); + $this->Render_Last(); + } else { + $this->Render_Next(false); + $this->Render_Last(false); + } + $s = ob_get_contents(); + ob_end_clean(); + return $s; + } + + //------------------- + // This is the footer + function RenderPageCount() + { + if (!$this->db->pageExecuteCountRows) return ''; + $lastPage = $this->rs->LastPageNo(); + if ($lastPage == -1) $lastPage = 1; // check for empty rs. + if ($this->curr_page > $lastPage) $this->curr_page = 1; + return "$this->page ".$this->curr_page."/".$lastPage.""; + } + + //----------------------------------- + // Call this class to draw everything. + function Render($rows=10) + { + global $ADODB_COUNTRECS; + + $this->rows = $rows; + + if ($this->db->dataProvider == 'informix') $this->db->cursorType = IFX_SCROLL; + + $savec = $ADODB_COUNTRECS; + if ($this->db->pageExecuteCountRows) $ADODB_COUNTRECS = true; + if ($this->cache) + $rs = $this->db->CachePageExecute($this->cache,$this->sql,$rows,$this->curr_page); + else + $rs = $this->db->PageExecute($this->sql,$rows,$this->curr_page); + $ADODB_COUNTRECS = $savec; + + $this->rs = $rs; + if (!$rs) { + print "

Query failed: $this->sql

"; + return; + } + + if (!$rs->EOF && (!$rs->AtFirstPage() || !$rs->AtLastPage())) + $header = $this->RenderNav(); + else + $header = " "; + + $grid = $this->RenderGrid(); + $footer = $this->RenderPageCount(); + + $this->RenderLayout($header,$grid,$footer); + + $rs->Close(); + $this->rs = false; + } + + //------------------------------------------------------ + // override this to control overall layout and formating + function RenderLayout($header,$grid,$footer,$attributes='border=1 bgcolor=beige') + { + echo "
", + $header, + "
", + $grid, + "
", + $footer, + "
"; + } +} diff --git a/app/vendor/adodb/adodb-php/adodb-pear.inc.php b/app/vendor/adodb/adodb-php/adodb-pear.inc.php new file mode 100644 index 000000000..c8f09331b --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-pear.inc.php @@ -0,0 +1,370 @@ + | + * and Tomas V.V.Cox . Portions (c)1997-2002 The PHP Group. + */ + + /* + We support: + + DB_Common + --------- + query - returns PEAR_Error on error + limitQuery - return PEAR_Error on error + prepare - does not return PEAR_Error on error + execute - does not return PEAR_Error on error + setFetchMode - supports ASSOC and ORDERED + errorNative + quote + nextID + disconnect + + getOne + getAssoc + getRow + getCol + getAll + + DB_Result + --------- + numRows - returns -1 if not supported + numCols + fetchInto - does not support passing of fetchmode + fetchRows - does not support passing of fetchmode + free + */ + +define('ADODB_PEAR',dirname(__FILE__)); +include_once "PEAR.php"; +include_once ADODB_PEAR."/adodb-errorpear.inc.php"; +include_once ADODB_PEAR."/adodb.inc.php"; + +if (!defined('DB_OK')) { +define("DB_OK", 1); +define("DB_ERROR",-1); + +/** + * This is a special constant that tells DB the user hasn't specified + * any particular get mode, so the default should be used. + */ + +define('DB_FETCHMODE_DEFAULT', 0); + +/** + * Column data indexed by numbers, ordered from 0 and up + */ + +define('DB_FETCHMODE_ORDERED', 1); + +/** + * Column data indexed by column names + */ + +define('DB_FETCHMODE_ASSOC', 2); + +/* for compatibility */ + +define('DB_GETMODE_ORDERED', DB_FETCHMODE_ORDERED); +define('DB_GETMODE_ASSOC', DB_FETCHMODE_ASSOC); + +/** + * these are constants for the tableInfo-function + * they are bitwised or'ed. so if there are more constants to be defined + * in the future, adjust DB_TABLEINFO_FULL accordingly + */ + +define('DB_TABLEINFO_ORDER', 1); +define('DB_TABLEINFO_ORDERTABLE', 2); +define('DB_TABLEINFO_FULL', 3); +} + +/** + * The main "DB" class is simply a container class with some static + * methods for creating DB objects as well as some utility functions + * common to all parts of DB. + * + */ + +class DB +{ + /** + * Create a new DB object for the specified database type + * + * @param $type string database type, for example "mysql" + * + * @return object a newly created DB object, or a DB error code on + * error + */ + + function factory($type) + { + include_once(ADODB_DIR."/drivers/adodb-$type.inc.php"); + $obj = NewADOConnection($type); + if (!is_object($obj)) $obj = new PEAR_Error('Unknown Database Driver: '.$dsninfo['phptype'],-1); + return $obj; + } + + /** + * Create a new DB object and connect to the specified database + * + * @param $dsn mixed "data source name", see the DB::parseDSN + * method for a description of the dsn format. Can also be + * specified as an array of the format returned by DB::parseDSN. + * + * @param $options mixed if boolean (or scalar), tells whether + * this connection should be persistent (for backends that support + * this). This parameter can also be an array of options, see + * DB_common::setOption for more information on connection + * options. + * + * @return object a newly created DB connection object, or a DB + * error object on error + * + * @see DB::parseDSN + * @see DB::isError + */ + function connect($dsn, $options = false) + { + if (is_array($dsn)) { + $dsninfo = $dsn; + } else { + $dsninfo = DB::parseDSN($dsn); + } + switch ($dsninfo["phptype"]) { + case 'pgsql': $type = 'postgres7'; break; + case 'ifx': $type = 'informix9'; break; + default: $type = $dsninfo["phptype"]; break; + } + + if (is_array($options) && isset($options["debug"]) && + $options["debug"] >= 2) { + // expose php errors with sufficient debug level + @include_once("adodb-$type.inc.php"); + } else { + @include_once("adodb-$type.inc.php"); + } + + @$obj = NewADOConnection($type); + if (!is_object($obj)) { + $obj = new PEAR_Error('Unknown Database Driver: '.$dsninfo['phptype'],-1); + return $obj; + } + if (is_array($options)) { + foreach($options as $k => $v) { + switch(strtolower($k)) { + case 'persist': + case 'persistent': $persist = $v; break; + #ibase + case 'dialect': $obj->dialect = $v; break; + case 'charset': $obj->charset = $v; break; + case 'buffers': $obj->buffers = $v; break; + #ado + case 'charpage': $obj->charPage = $v; break; + #mysql + case 'clientflags': $obj->clientFlags = $v; break; + } + } + } else { + $persist = false; + } + + if (isset($dsninfo['socket'])) $dsninfo['hostspec'] .= ':'.$dsninfo['socket']; + else if (isset($dsninfo['port'])) $dsninfo['hostspec'] .= ':'.$dsninfo['port']; + + if($persist) $ok = $obj->PConnect($dsninfo['hostspec'], $dsninfo['username'],$dsninfo['password'],$dsninfo['database']); + else $ok = $obj->Connect($dsninfo['hostspec'], $dsninfo['username'],$dsninfo['password'],$dsninfo['database']); + + if (!$ok) $obj = ADODB_PEAR_Error(); + return $obj; + } + + /** + * Return the DB API version + * + * @return int the DB API version number + */ + function apiVersion() + { + return 2; + } + + /** + * Tell whether a result code from a DB method is an error + * + * @param $value int result code + * + * @return bool whether $value is an error + */ + function isError($value) + { + if (!is_object($value)) return false; + $class = strtolower(get_class($value)); + return $class == 'pear_error' || is_subclass_of($value, 'pear_error') || + $class == 'db_error' || is_subclass_of($value, 'db_error'); + } + + + /** + * Tell whether a result code from a DB method is a warning. + * Warnings differ from errors in that they are generated by DB, + * and are not fatal. + * + * @param $value mixed result value + * + * @return bool whether $value is a warning + */ + function isWarning($value) + { + return false; + /* + return is_object($value) && + (get_class( $value ) == "db_warning" || + is_subclass_of($value, "db_warning"));*/ + } + + /** + * Parse a data source name + * + * @param $dsn string Data Source Name to be parsed + * + * @return array an associative array with the following keys: + * + * phptype: Database backend used in PHP (mysql, odbc etc.) + * dbsyntax: Database used with regards to SQL syntax etc. + * protocol: Communication protocol to use (tcp, unix etc.) + * hostspec: Host specification (hostname[:port]) + * database: Database to use on the DBMS server + * username: User name for login + * password: Password for login + * + * The format of the supplied DSN is in its fullest form: + * + * phptype(dbsyntax)://username:password@protocol+hostspec/database + * + * Most variations are allowed: + * + * phptype://username:password@protocol+hostspec:110//usr/db_file.db + * phptype://username:password@hostspec/database_name + * phptype://username:password@hostspec + * phptype://username@hostspec + * phptype://hostspec/database + * phptype://hostspec + * phptype(dbsyntax) + * phptype + * + * @author Tomas V.V.Cox + */ + function parseDSN($dsn) + { + if (is_array($dsn)) { + return $dsn; + } + + $parsed = array( + 'phptype' => false, + 'dbsyntax' => false, + 'protocol' => false, + 'hostspec' => false, + 'database' => false, + 'username' => false, + 'password' => false + ); + + // Find phptype and dbsyntax + if (($pos = strpos($dsn, '://')) !== false) { + $str = substr($dsn, 0, $pos); + $dsn = substr($dsn, $pos + 3); + } else { + $str = $dsn; + $dsn = NULL; + } + + // Get phptype and dbsyntax + // $str => phptype(dbsyntax) + if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) { + $parsed['phptype'] = $arr[1]; + $parsed['dbsyntax'] = (empty($arr[2])) ? $arr[1] : $arr[2]; + } else { + $parsed['phptype'] = $str; + $parsed['dbsyntax'] = $str; + } + + if (empty($dsn)) { + return $parsed; + } + + // Get (if found): username and password + // $dsn => username:password@protocol+hostspec/database + if (($at = strpos($dsn,'@')) !== false) { + $str = substr($dsn, 0, $at); + $dsn = substr($dsn, $at + 1); + if (($pos = strpos($str, ':')) !== false) { + $parsed['username'] = urldecode(substr($str, 0, $pos)); + $parsed['password'] = urldecode(substr($str, $pos + 1)); + } else { + $parsed['username'] = urldecode($str); + } + } + + // Find protocol and hostspec + // $dsn => protocol+hostspec/database + if (($pos=strpos($dsn, '/')) !== false) { + $str = substr($dsn, 0, $pos); + $dsn = substr($dsn, $pos + 1); + } else { + $str = $dsn; + $dsn = NULL; + } + + // Get protocol + hostspec + // $str => protocol+hostspec + if (($pos=strpos($str, '+')) !== false) { + $parsed['protocol'] = substr($str, 0, $pos); + $parsed['hostspec'] = urldecode(substr($str, $pos + 1)); + } else { + $parsed['hostspec'] = urldecode($str); + } + + // Get dabase if any + // $dsn => database + if (!empty($dsn)) { + $parsed['database'] = $dsn; + } + + return $parsed; + } + + /** + * Load a PHP database extension if it is not loaded already. + * + * @access public + * + * @param $name the base name of the extension (without the .so or + * .dll suffix) + * + * @return bool true if the extension was already or successfully + * loaded, false if it could not be loaded + */ + function assertExtension($name) + { + if (!extension_loaded($name)) { + $dlext = (strncmp(PHP_OS,'WIN',3) === 0) ? '.dll' : '.so'; + @dl($name . $dlext); + } + if (!extension_loaded($name)) { + return false; + } + return true; + } +} diff --git a/app/vendor/adodb/adodb-php/adodb-perf.inc.php b/app/vendor/adodb/adodb-php/adodb-perf.inc.php new file mode 100644 index 000000000..69218c6e0 --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-perf.inc.php @@ -0,0 +1,1099 @@ += minimum number of secs to run + + +// returns in K the memory of current process, or 0 if not known +function adodb_getmem() +{ + if (function_exists('memory_get_usage')) + return (integer) ((memory_get_usage()+512)/1024); + + $pid = getmypid(); + + if ( strncmp(strtoupper(PHP_OS),'WIN',3)==0) { + $output = array(); + + exec('tasklist /FI "PID eq ' . $pid. '" /FO LIST', $output); + return substr($output[5], strpos($output[5], ':') + 1); + } + + /* Hopefully UNIX */ + exec("ps --pid $pid --no-headers -o%mem,size", $output); + if (sizeof($output) == 0) return 0; + + $memarr = explode(' ',$output[0]); + if (sizeof($memarr)>=2) return (integer) $memarr[1]; + + return 0; +} + +// avoids localization problems where , is used instead of . +function adodb_round($n,$prec) +{ + return number_format($n, $prec, '.', ''); +} + +/* obsolete: return microtime value as a float. Retained for backward compat */ +function adodb_microtime() +{ + return microtime(true); +} + +/* sql code timing */ +function adodb_log_sql(&$connx,$sql,$inputarr) +{ + $perf_table = adodb_perf::table(); + $connx->fnExecute = false; + $a0 = microtime(true); + $rs = $connx->Execute($sql,$inputarr); + $a1 = microtime(true); + + if (!empty($connx->_logsql) && (empty($connx->_logsqlErrors) || !$rs)) { + global $ADODB_LOG_CONN; + + if (!empty($ADODB_LOG_CONN)) { + $conn = $ADODB_LOG_CONN; + if ($conn->databaseType != $connx->databaseType) + $prefix = '/*dbx='.$connx->databaseType .'*/ '; + else + $prefix = ''; + } else { + $conn = $connx; + $prefix = ''; + } + + $conn->_logsql = false; // disable logsql error simulation + $dbT = $conn->databaseType; + + $time = $a1 - $a0; + + if (!$rs) { + $errM = $connx->ErrorMsg(); + $errN = $connx->ErrorNo(); + $conn->lastInsID = 0; + $tracer = substr('ERROR: '.htmlspecialchars($errM),0,250); + } else { + $tracer = ''; + $errM = ''; + $errN = 0; + $dbg = $conn->debug; + $conn->debug = false; + if (!is_object($rs) || $rs->dataProvider == 'empty') + $conn->_affected = $conn->affected_rows(true); + $conn->lastInsID = @$conn->Insert_ID(); + $conn->debug = $dbg; + } + if (isset($_SERVER['HTTP_HOST'])) { + $tracer .= '
'.$_SERVER['HTTP_HOST']; + if (isset($_SERVER['PHP_SELF'])) $tracer .= htmlspecialchars($_SERVER['PHP_SELF']); + } else + if (isset($_SERVER['PHP_SELF'])) $tracer .= '
'.htmlspecialchars($_SERVER['PHP_SELF']); + //$tracer .= (string) adodb_backtrace(false); + + $tracer = (string) substr($tracer,0,500); + + if (is_array($inputarr)) { + if (is_array(reset($inputarr))) $params = 'Array sizeof='.sizeof($inputarr); + else { + // Quote string parameters so we can see them in the + // performance stats. This helps spot disabled indexes. + $xar_params = $inputarr; + foreach ($xar_params as $xar_param_key => $xar_param) { + if (gettype($xar_param) == 'string') + $xar_params[$xar_param_key] = '"' . $xar_param . '"'; + } + $params = implode(', ', $xar_params); + if (strlen($params) >= 3000) $params = substr($params, 0, 3000); + } + } else { + $params = ''; + } + + if (is_array($sql)) $sql = $sql[0]; + if ($prefix) $sql = $prefix.$sql; + $arr = array('b'=>strlen($sql).'.'.crc32($sql), + 'c'=>substr($sql,0,3900), 'd'=>$params,'e'=>$tracer,'f'=>adodb_round($time,6)); + //var_dump($arr); + $saved = $conn->debug; + $conn->debug = 0; + + $d = $conn->sysTimeStamp; + if (empty($d)) $d = date("'Y-m-d H:i:s'"); + if ($conn->dataProvider == 'oci8' && $dbT != 'oci8po') { + $isql = "insert into $perf_table values($d,:b,:c,:d,:e,:f)"; + } else if ($dbT == 'odbc_mssql' || $dbT == 'informix' || strncmp($dbT,'odbtp',4)==0) { + $timer = $arr['f']; + if ($dbT == 'informix') $sql2 = substr($sql2,0,230); + + $sql1 = $conn->qstr($arr['b']); + $sql2 = $conn->qstr($arr['c']); + $params = $conn->qstr($arr['d']); + $tracer = $conn->qstr($arr['e']); + + $isql = "insert into $perf_table (created,sql0,sql1,params,tracer,timer) values($d,$sql1,$sql2,$params,$tracer,$timer)"; + if ($dbT == 'informix') $isql = str_replace(chr(10),' ',$isql); + $arr = false; + } else { + if ($dbT == 'db2') $arr['f'] = (float) $arr['f']; + $isql = "insert into $perf_table (created,sql0,sql1,params,tracer,timer) values( $d,?,?,?,?,?)"; + } + + global $ADODB_PERF_MIN; + if ($errN != 0 || $time >= $ADODB_PERF_MIN) { + $ok = $conn->Execute($isql,$arr); + } else + $ok = true; + + $conn->debug = $saved; + + if ($ok) { + $conn->_logsql = true; + } else { + $err2 = $conn->ErrorMsg(); + $conn->_logsql = true; // enable logsql error simulation + $perf = NewPerfMonitor($conn); + if ($perf) { + if ($perf->CreateLogTable()) $ok = $conn->Execute($isql,$arr); + } else { + $ok = $conn->Execute("create table $perf_table ( + created varchar(50), + sql0 varchar(250), + sql1 varchar(4000), + params varchar(3000), + tracer varchar(500), + timer decimal(16,6))"); + } + if (!$ok) { + ADOConnection::outp( "

LOGSQL Insert Failed: $isql
$err2

"); + $conn->_logsql = false; + } + } + $connx->_errorMsg = $errM; + $connx->_errorCode = $errN; + } + $connx->fnExecute = 'adodb_log_sql'; + return $rs; +} + + +/* +The settings data structure is an associative array that database parameter per element. + +Each database parameter element in the array is itself an array consisting of: + +0: category code, used to group related db parameters +1: either + a. sql string to retrieve value, eg. "select value from v\$parameter where name='db_block_size'", + b. array holding sql string and field to look for, e.g. array('show variables','table_cache'), + c. a string prefixed by =, then a PHP method of the class is invoked, + e.g. to invoke $this->GetIndexValue(), set this array element to '=GetIndexValue', +2: description of the database parameter +*/ + +class adodb_perf { + var $conn; + var $color = '#F0F0F0'; + var $table = ''; + var $titles = ''; + var $warnRatio = 90; + var $tablesSQL = false; + var $cliFormat = "%32s => %s \r\n"; + var $sql1 = 'sql1'; // used for casting sql1 to text for mssql + var $explain = true; + var $helpurl = 'LogSQL help'; + var $createTableSQL = false; + var $maxLength = 2000; + + // Sets the tablename to be used + static function table($newtable = false) + { + static $_table; + + if (!empty($newtable)) $_table = $newtable; + if (empty($_table)) $_table = 'adodb_logsql'; + return $_table; + } + + // returns array with info to calculate CPU Load + function _CPULoad() + { +/* + +cpu 524152 2662 2515228 336057010 +cpu0 264339 1408 1257951 168025827 +cpu1 259813 1254 1257277 168031181 +page 622307 25475680 +swap 24 1891 +intr 890153570 868093576 6 0 4 4 0 6 1 2 0 0 0 124 0 8098760 2 13961053 0 0 0 0 0 0 0 0 0 0 0 0 0 16 16 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +disk_io: (3,0):(3144904,54369,610378,3090535,50936192) (3,1):(3630212,54097,633016,3576115,50951320) +ctxt 66155838 +btime 1062315585 +processes 69293 + +*/ + // Algorithm is taken from + // http://social.technet.microsoft.com/Forums/en-US/winservergen/thread/414b0e1b-499c-411e-8a02-6a12e339c0f1/ + if (strncmp(PHP_OS,'WIN',3)==0) { + if (PHP_VERSION == '5.0.0') return false; + if (PHP_VERSION == '5.0.1') return false; + if (PHP_VERSION == '5.0.2') return false; + if (PHP_VERSION == '5.0.3') return false; + if (PHP_VERSION == '4.3.10') return false; # see http://bugs.php.net/bug.php?id=31737 + + static $FAIL = false; + if ($FAIL) return false; + + $objName = "winmgmts:{impersonationLevel=impersonate}!\\\\.\\root\\CIMV2"; + $myQuery = "SELECT * FROM Win32_PerfFormattedData_PerfOS_Processor WHERE Name = '_Total'"; + + try { + @$objWMIService = new COM($objName); + if (!$objWMIService) { + $FAIL = true; + return false; + } + + $info[0] = -1; + $info[1] = 0; + $info[2] = 0; + $info[3] = 0; + foreach($objWMIService->ExecQuery($myQuery) as $objItem) { + $info[0] = $objItem->PercentProcessorTime(); + } + + } catch(Exception $e) { + $FAIL = true; + echo $e->getMessage(); + return false; + } + + return $info; + } + + // Algorithm - Steve Blinch (BlitzAffe Online, http://www.blitzaffe.com) + $statfile = '/proc/stat'; + if (!file_exists($statfile)) return false; + + $fd = fopen($statfile,"r"); + if (!$fd) return false; + + $statinfo = explode("\n",fgets($fd, 1024)); + fclose($fd); + foreach($statinfo as $line) { + $info = explode(" ",$line); + if($info[0]=="cpu") { + array_shift($info); // pop off "cpu" + if(!$info[0]) array_shift($info); // pop off blank space (if any) + return $info; + } + } + + return false; + + } + + /* NOT IMPLEMENTED */ + function MemInfo() + { + /* + + total: used: free: shared: buffers: cached: +Mem: 1055289344 917299200 137990144 0 165437440 599773184 +Swap: 2146775040 11055104 2135719936 +MemTotal: 1030556 kB +MemFree: 134756 kB +MemShared: 0 kB +Buffers: 161560 kB +Cached: 581384 kB +SwapCached: 4332 kB +Active: 494468 kB +Inact_dirty: 322856 kB +Inact_clean: 24256 kB +Inact_target: 168316 kB +HighTotal: 131064 kB +HighFree: 1024 kB +LowTotal: 899492 kB +LowFree: 133732 kB +SwapTotal: 2096460 kB +SwapFree: 2085664 kB +Committed_AS: 348732 kB + */ + } + + + /* + Remember that this is client load, not db server load! + */ + var $_lastLoad; + function CPULoad() + { + $info = $this->_CPULoad(); + if (!$info) return false; + + if (strncmp(PHP_OS,'WIN',3)==0) { + return (integer) $info[0]; + }else { + if (empty($this->_lastLoad)) { + sleep(1); + $this->_lastLoad = $info; + $info = $this->_CPULoad(); + } + + $last = $this->_lastLoad; + $this->_lastLoad = $info; + + $d_user = $info[0] - $last[0]; + $d_nice = $info[1] - $last[1]; + $d_system = $info[2] - $last[2]; + $d_idle = $info[3] - $last[3]; + + //printf("Delta - User: %f Nice: %f System: %f Idle: %f
",$d_user,$d_nice,$d_system,$d_idle); + + $total=$d_user+$d_nice+$d_system+$d_idle; + if ($total<1) $total=1; + return 100*($d_user+$d_nice+$d_system)/$total; + } + } + + function Tracer($sql) + { + $perf_table = adodb_perf::table(); + $saveE = $this->conn->fnExecute; + $this->conn->fnExecute = false; + + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->conn->fetchMode !== false) $savem = $this->conn->SetFetchMode(false); + + $sqlq = $this->conn->qstr($sql); + $arr = $this->conn->GetArray( +"select count(*),tracer + from $perf_table where sql1=$sqlq + group by tracer + order by 1 desc"); + $s = ''; + if ($arr) { + $s .= '

Scripts Affected

'; + foreach($arr as $k) { + $s .= sprintf("%4d",$k[0]).'   '.strip_tags($k[1]).'
'; + } + } + + if (isset($savem)) $this->conn->SetFetchMode($savem); + $ADODB_CACHE_MODE = $save; + $this->conn->fnExecute = $saveE; + return $s; + } + + /* + Explain Plan for $sql. + If only a snippet of the $sql is passed in, then $partial will hold the crc32 of the + actual sql. + */ + function Explain($sql,$partial=false) + { + return false; + } + + function InvalidSQL($numsql = 10) + { + + if (isset($_GET['sql'])) return; + $s = '

Invalid SQL

'; + $saveE = $this->conn->fnExecute; + $this->conn->fnExecute = false; + $perf_table = adodb_perf::table(); + $rs = $this->conn->SelectLimit("select distinct count(*),sql1,tracer as error_msg from $perf_table where tracer like 'ERROR:%' group by sql1,tracer order by 1 desc",$numsql);//,$numsql); + $this->conn->fnExecute = $saveE; + if ($rs) { + $s .= rs2html($rs,false,false,false,false); + } else + return "

$this->helpurl. ".$this->conn->ErrorMsg()."

"; + + return $s; + } + + + /* + This script identifies the longest running SQL + */ + function _SuspiciousSQL($numsql = 10) + { + global $ADODB_FETCH_MODE; + + $perf_table = adodb_perf::table(); + $saveE = $this->conn->fnExecute; + $this->conn->fnExecute = false; + + if (isset($_GET['exps']) && isset($_GET['sql'])) { + $partial = !empty($_GET['part']); + echo "".$this->Explain($_GET['sql'],$partial)."\n"; + } + + if (isset($_GET['sql'])) return; + $sql1 = $this->sql1; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->conn->fetchMode !== false) $savem = $this->conn->SetFetchMode(false); + //$this->conn->debug=1; + $rs = $this->conn->SelectLimit( + "select avg(timer) as avg_timer,$sql1,count(*),max(timer) as max_timer,min(timer) as min_timer + from $perf_table + where {$this->conn->upperCase}({$this->conn->substr}(sql0,1,5)) not in ('DROP ','INSER','COMMI','CREAT') + and (tracer is null or tracer not like 'ERROR:%') + group by sql1 + order by 1 desc",$numsql); + if (isset($savem)) $this->conn->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + $this->conn->fnExecute = $saveE; + + if (!$rs) return "

$this->helpurl. ".$this->conn->ErrorMsg()."

"; + $s = "

Suspicious SQL

+The following SQL have high average execution times
+
ParameterValueDescription
\n"; + $max = $this->maxLength; + while (!$rs->EOF) { + $sql = $rs->fields[1]; + $raw = urlencode($sql); + if (strlen($raw)>$max-100) { + $sql2 = substr($sql,0,$max-500); + $raw = urlencode($sql2).'&part='.crc32($sql); + } + $prefix = ""; + $suffix = ""; + if ($this->explain == false || strlen($prefix)>$max) { + $suffix = ' ... String too long for GET parameter: '.strlen($prefix).''; + $prefix = ''; + } + $s .= ""; + $rs->MoveNext(); + } + return $s."
Avg TimeCountSQLMaxMin
".adodb_round($rs->fields[0],6)."".$rs->fields[2]."".$prefix.htmlspecialchars($sql).$suffix."". + "".$rs->fields[3]."".$rs->fields[4]."
"; + + } + + function CheckMemory() + { + return ''; + } + + + function SuspiciousSQL($numsql=10) + { + return adodb_perf::_SuspiciousSQL($numsql); + } + + function ExpensiveSQL($numsql=10) + { + return adodb_perf::_ExpensiveSQL($numsql); + } + + + /* + This reports the percentage of load on the instance due to the most + expensive few SQL statements. Tuning these statements can often + make huge improvements in overall system performance. + */ + function _ExpensiveSQL($numsql = 10) + { + global $ADODB_FETCH_MODE; + + $perf_table = adodb_perf::table(); + $saveE = $this->conn->fnExecute; + $this->conn->fnExecute = false; + + if (isset($_GET['expe']) && isset($_GET['sql'])) { + $partial = !empty($_GET['part']); + echo "".$this->Explain($_GET['sql'],$partial)."\n"; + } + + if (isset($_GET['sql'])) return; + + $sql1 = $this->sql1; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->conn->fetchMode !== false) $savem = $this->conn->SetFetchMode(false); + + $rs = $this->conn->SelectLimit( + "select sum(timer) as total,$sql1,count(*),max(timer) as max_timer,min(timer) as min_timer + from $perf_table + where {$this->conn->upperCase}({$this->conn->substr}(sql0,1,5)) not in ('DROP ','INSER','COMMI','CREAT') + and (tracer is null or tracer not like 'ERROR:%') + group by sql1 + having count(*)>1 + order by 1 desc",$numsql); + if (isset($savem)) $this->conn->SetFetchMode($savem); + $this->conn->fnExecute = $saveE; + $ADODB_FETCH_MODE = $save; + if (!$rs) return "

$this->helpurl. ".$this->conn->ErrorMsg()."

"; + $s = "

Expensive SQL

+Tuning the following SQL could reduce the server load substantially
+\n"; + $max = $this->maxLength; + while (!$rs->EOF) { + $sql = $rs->fields[1]; + $raw = urlencode($sql); + if (strlen($raw)>$max-100) { + $sql2 = substr($sql,0,$max-500); + $raw = urlencode($sql2).'&part='.crc32($sql); + } + $prefix = ""; + $suffix = ""; + if($this->explain == false || strlen($prefix>$max)) { + $prefix = ''; + $suffix = ''; + } + $s .= ""; + $rs->MoveNext(); + } + return $s."
LoadCountSQLMaxMin
".adodb_round($rs->fields[0],6)."".$rs->fields[2]."".$prefix.htmlspecialchars($sql).$suffix."". + "".$rs->fields[3]."".$rs->fields[4]."
"; + } + + /* + Raw function to return parameter value from $settings. + */ + function DBParameter($param) + { + if (empty($this->settings[$param])) return false; + $sql = $this->settings[$param][1]; + return $this->_DBParameter($sql); + } + + /* + Raw function returning array of poll paramters + */ + function PollParameters() + { + $arr[0] = (float)$this->DBParameter('data cache hit ratio'); + $arr[1] = (float)$this->DBParameter('data reads'); + $arr[2] = (float)$this->DBParameter('data writes'); + $arr[3] = (integer) $this->DBParameter('current connections'); + return $arr; + } + + /* + Low-level Get Database Parameter + */ + function _DBParameter($sql) + { + $savelog = $this->conn->LogSQL(false); + if (is_array($sql)) { + global $ADODB_FETCH_MODE; + + $sql1 = $sql[0]; + $key = $sql[1]; + if (sizeof($sql)>2) $pos = $sql[2]; + else $pos = 1; + if (sizeof($sql)>3) $coef = $sql[3]; + else $coef = false; + $ret = false; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->conn->fetchMode !== false) $savem = $this->conn->SetFetchMode(false); + + $rs = $this->conn->Execute($sql1); + + if (isset($savem)) $this->conn->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + if ($rs) { + while (!$rs->EOF) { + $keyf = reset($rs->fields); + if (trim($keyf) == $key) { + $ret = $rs->fields[$pos]; + if ($coef) $ret *= $coef; + break; + } + $rs->MoveNext(); + } + $rs->Close(); + } + $this->conn->LogSQL($savelog); + return $ret; + } else { + if (strncmp($sql,'=',1) == 0) { + $fn = substr($sql,1); + return $this->$fn(); + } + $sql = str_replace('$DATABASE',$this->conn->database,$sql); + $ret = $this->conn->GetOne($sql); + $this->conn->LogSQL($savelog); + + return $ret; + } + } + + /* + Warn if cache ratio falls below threshold. Displayed in "Description" column. + */ + function WarnCacheRatio($val) + { + if ($val < $this->warnRatio) + return 'Cache ratio should be at least '.$this->warnRatio.'%'; + else return ''; + } + + function clearsql() + { + $perf_table = adodb_perf::table(); + $this->conn->Execute("delete from $perf_table where created<".$this->conn->sysTimeStamp); + } + /***********************************************************************************************/ + // HIGH LEVEL UI FUNCTIONS + /***********************************************************************************************/ + + + function UI($pollsecs=5) + { + global $ADODB_LOG_CONN; + + $perf_table = adodb_perf::table(); + $conn = $this->conn; + + $app = $conn->host; + if ($conn->host && $conn->database) $app .= ', db='; + $app .= $conn->database; + + if ($app) $app .= ', '; + $savelog = $this->conn->LogSQL(false); + $info = $conn->ServerInfo(); + if (isset($_GET['clearsql'])) { + $this->clearsql(); + } + $this->conn->LogSQL($savelog); + + // magic quotes + + if (isset($_GET['sql']) && get_magic_quotes_gpc()) { + $_GET['sql'] = $_GET['sql'] = str_replace(array("\\'",'\"'),array("'",'"'),$_GET['sql']); + } + + if (!isset($_SESSION['ADODB_PERF_SQL'])) $nsql = $_SESSION['ADODB_PERF_SQL'] = 10; + else $nsql = $_SESSION['ADODB_PERF_SQL']; + + $app .= $info['description']; + + + if (isset($_GET['do'])) $do = $_GET['do']; + else if (isset($_POST['do'])) $do = $_POST['do']; + else if (isset($_GET['sql'])) $do = 'viewsql'; + else $do = 'stats'; + + if (isset($_GET['nsql'])) { + if ($_GET['nsql'] > 0) $nsql = $_SESSION['ADODB_PERF_SQL'] = (integer) $_GET['nsql']; + } + echo "ADOdb Performance Monitor on $app"; + if ($do == 'viewsql') $form = "
# SQL:
"; + else $form = " "; + + $allowsql = !defined('ADODB_PERF_NO_RUN_SQL'); + global $ADODB_PERF_MIN; + $app .= " (Min sql timing \$ADODB_PERF_MIN=$ADODB_PERF_MIN secs)"; + + if (empty($_GET['hidem'])) + echo "
+ ADOdb Performance Monitor for $app
+ Performance Stats   View SQL +   View Tables   Poll Stats", + $allowsql ? '   Run SQL' : '', + "$form", + "
"; + + + switch ($do) { + default: + case 'stats': + if (empty($ADODB_LOG_CONN)) + echo "

  Clear SQL Log
"; + echo $this->HealthCheck(); + //$this->conn->debug=1; + echo $this->CheckMemory(); + break; + case 'poll': + $self = htmlspecialchars($_SERVER['PHP_SELF']); + echo ""; + break; + case 'poll2': + echo "

";
+			$this->Poll($pollsecs);
+			break;
+
+		case 'dosql':
+			if (!$allowsql) break;
+
+			$this->DoSQLForm();
+			break;
+		case 'viewsql':
+			if (empty($_GET['hidem']))
+				echo "  Clear SQL Log
"; + echo($this->SuspiciousSQL($nsql)); + echo($this->ExpensiveSQL($nsql)); + echo($this->InvalidSQL($nsql)); + break; + case 'tables': + echo $this->Tables(); break; + } + global $ADODB_vers; + echo "

$ADODB_vers Sponsored by phpLens
"; + } + + /* + Runs in infinite loop, returning real-time statistics + */ + function Poll($secs=5) + { + $this->conn->fnExecute = false; + //$this->conn->debug=1; + if ($secs <= 1) $secs = 1; + echo "Accumulating statistics, every $secs seconds...\n";flush(); + $arro = $this->PollParameters(); + $cnt = 0; + set_time_limit(0); + sleep($secs); + while (1) { + + $arr = $this->PollParameters(); + + $hits = sprintf('%2.2f',$arr[0]); + $reads = sprintf('%12.4f',($arr[1]-$arro[1])/$secs); + $writes = sprintf('%12.4f',($arr[2]-$arro[2])/$secs); + $sess = sprintf('%5d',$arr[3]); + + $load = $this->CPULoad(); + if ($load !== false) { + $oslabel = 'WS-CPU%'; + $osval = sprintf(" %2.1f ",(float) $load); + }else { + $oslabel = ''; + $osval = ''; + } + if ($cnt % 10 == 0) echo " Time ".$oslabel." Hit% Sess Reads/s Writes/s\n"; + $cnt += 1; + echo date('H:i:s').' '.$osval."$hits $sess $reads $writes\n"; + flush(); + + if (connection_aborted()) return; + + sleep($secs); + $arro = $arr; + } + } + + /* + Returns basic health check in a command line interface + */ + function HealthCheckCLI() + { + return $this->HealthCheck(true); + } + + + /* + Returns basic health check as HTML + */ + function HealthCheck($cli=false) + { + $saveE = $this->conn->fnExecute; + $this->conn->fnExecute = false; + if ($cli) $html = ''; + else $html = $this->table.'

'.$this->conn->databaseType.'

'.$this->titles; + + $oldc = false; + $bgc = ''; + foreach($this->settings as $name => $arr) { + if ($arr === false) break; + + if (!is_string($name)) { + if ($cli) $html .= " -- $arr -- \n"; + else $html .= "color>$arr  "; + continue; + } + + if (!is_array($arr)) break; + $category = $arr[0]; + $how = $arr[1]; + if (sizeof($arr)>2) $desc = $arr[2]; + else $desc = '   '; + + + if ($category == 'HIDE') continue; + + $val = $this->_DBParameter($how); + + if ($desc && strncmp($desc,"=",1) === 0) { + $fn = substr($desc,1); + $desc = $this->$fn($val); + } + + if ($val === false) { + $m = $this->conn->ErrorMsg(); + $val = "Error: $m"; + } else { + if (is_numeric($val) && $val >= 256*1024) { + if ($val % (1024*1024) == 0) { + $val /= (1024*1024); + $val .= 'M'; + } else if ($val % 1024 == 0) { + $val /= 1024; + $val .= 'K'; + } + //$val = htmlspecialchars($val); + } + } + if ($category != $oldc) { + $oldc = $category; + //$bgc = ($bgc == ' bgcolor='.$this->color) ? ' bgcolor=white' : ' bgcolor='.$this->color; + } + if (strlen($desc)==0) $desc = ' '; + if (strlen($val)==0) $val = ' '; + if ($cli) { + $html .= str_replace(' ','',sprintf($this->cliFormat,strip_tags($name),strip_tags($val),strip_tags($desc))); + + }else { + $html .= "".$name.''.$val.''.$desc."\n"; + } + } + + if (!$cli) $html .= "\n"; + $this->conn->fnExecute = $saveE; + + return $html; + } + + function Tables($orderby='1') + { + if (!$this->tablesSQL) return false; + + $savelog = $this->conn->LogSQL(false); + $rs = $this->conn->Execute($this->tablesSQL.' order by '.$orderby); + $this->conn->LogSQL($savelog); + $html = rs2html($rs,false,false,false,false); + return $html; + } + + + function CreateLogTable() + { + if (!$this->createTableSQL) return false; + + $table = $this->table(); + $sql = str_replace('adodb_logsql',$table,$this->createTableSQL); + $savelog = $this->conn->LogSQL(false); + $ok = $this->conn->Execute($sql); + $this->conn->LogSQL($savelog); + return ($ok) ? true : false; + } + + function DoSQLForm() + { + + + $PHP_SELF = htmlspecialchars($_SERVER['PHP_SELF']); + $sql = isset($_REQUEST['sql']) ? $_REQUEST['sql'] : ''; + + if (isset($_SESSION['phplens_sqlrows'])) $rows = $_SESSION['phplens_sqlrows']; + else $rows = 3; + + if (isset($_REQUEST['SMALLER'])) { + $rows /= 2; + if ($rows < 3) $rows = 3; + $_SESSION['phplens_sqlrows'] = $rows; + } + if (isset($_REQUEST['BIGGER'])) { + $rows *= 2; + $_SESSION['phplens_sqlrows'] = $rows; + } + +?> + +
+ + + + + + +
Form size: + + +
+
+
+ +undomq(trim($sql)); + if (substr($sql,strlen($sql)-1) === ';') { + $print = true; + $sqla = $this->SplitSQL($sql); + } else { + $print = false; + $sqla = array($sql); + } + foreach($sqla as $sqls) { + + if (!$sqls) continue; + + if ($print) { + print "

".htmlspecialchars($sqls)."

"; + flush(); + } + $savelog = $this->conn->LogSQL(false); + $rs = $this->conn->Execute($sqls); + $this->conn->LogSQL($savelog); + if ($rs && is_object($rs) && !$rs->EOF) { + rs2html($rs); + while ($rs->NextRecordSet()) { + print "
 
"; + rs2html($rs); + } + } else { + $e1 = (integer) $this->conn->ErrorNo(); + $e2 = $this->conn->ErrorMsg(); + if (($e1) || ($e2)) { + if (empty($e1)) $e1 = '-1'; // postgresql fix + print '   '.$e1.': '.$e2; + } else { + print "

No Recordset returned

"; + } + } + } // foreach + } + + function SplitSQL($sql) + { + $arr = explode(';',$sql); + return $arr; + } + + function undomq($m) + { + if (get_magic_quotes_gpc()) { + // undo the damage + $m = str_replace('\\\\','\\',$m); + $m = str_replace('\"','"',$m); + $m = str_replace('\\\'','\'',$m); + } + return $m; +} + + + /************************************************************************/ + + /** + * Reorganise multiple table-indices/statistics/.. + * OptimizeMode could be given by last Parameter + * + * @example + *
+     *          optimizeTables( 'tableA');
+     *      
+ *
+     *          optimizeTables( 'tableA', 'tableB', 'tableC');
+     *      
+ *
+     *          optimizeTables( 'tableA', 'tableB', ADODB_OPT_LOW);
+     *      
+ * + * @param string table name of the table to optimize + * @param int mode optimization-mode + * ADODB_OPT_HIGH for full optimization + * ADODB_OPT_LOW for CPU-less optimization + * Default is LOW ADODB_OPT_LOW + * @author Markus Staab + * @return Returns true on success and false on error + */ + function OptimizeTables() + { + $args = func_get_args(); + $numArgs = func_num_args(); + + if ( $numArgs == 0) return false; + + $mode = ADODB_OPT_LOW; + $lastArg = $args[ $numArgs - 1]; + if ( !is_string($lastArg)) { + $mode = $lastArg; + unset( $args[ $numArgs - 1]); + } + + foreach( $args as $table) { + $this->optimizeTable( $table, $mode); + } + } + + /** + * Reorganise the table-indices/statistics/.. depending on the given mode. + * Default Implementation throws an error. + * + * @param string table name of the table to optimize + * @param int mode optimization-mode + * ADODB_OPT_HIGH for full optimization + * ADODB_OPT_LOW for CPU-less optimization + * Default is LOW ADODB_OPT_LOW + * @author Markus Staab + * @return Returns true on success and false on error + */ + function OptimizeTable( $table, $mode = ADODB_OPT_LOW) + { + ADOConnection::outp( sprintf( "

%s: '%s' not implemented for driver '%s'

", __CLASS__, __FUNCTION__, $this->conn->databaseType)); + return false; + } + + /** + * Reorganise current database. + * Default implementation loops over all MetaTables() and + * optimize each using optmizeTable() + * + * @author Markus Staab + * @return Returns true on success and false on error + */ + function optimizeDatabase() + { + $conn = $this->conn; + if ( !$conn) return false; + + $tables = $conn->MetaTables( 'TABLES'); + if ( !$tables ) return false; + + foreach( $tables as $table) { + if ( !$this->optimizeTable( $table)) { + return false; + } + } + + return true; + } + // end hack +} diff --git a/app/vendor/adodb/adodb-php/adodb-php4.inc.php b/app/vendor/adodb/adodb-php/adodb-php4.inc.php new file mode 100644 index 000000000..132f25d0a --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-php4.inc.php @@ -0,0 +1,16 @@ + 4 digit year conversion. The maximum is billions of years in the +future, but this is a theoretical limit as the computation of that year +would take too long with the current implementation of adodb_mktime(). + +This library replaces native functions as follows: + +
+	getdate()  with  adodb_getdate()
+	date()     with  adodb_date()
+	gmdate()   with  adodb_gmdate()
+	mktime()   with  adodb_mktime()
+	gmmktime() with  adodb_gmmktime()
+	strftime() with  adodb_strftime()
+	strftime() with  adodb_gmstrftime()
+
+ +The parameters are identical, except that adodb_date() accepts a subset +of date()'s field formats. Mktime() will convert from local time to GMT, +and date() will convert from GMT to local time, but daylight savings is +not handled currently. + +This library is independant of the rest of ADOdb, and can be used +as standalone code. + +PERFORMANCE + +For high speed, this library uses the native date functions where +possible, and only switches to PHP code when the dates fall outside +the 32-bit signed integer range. + +GREGORIAN CORRECTION + +Pope Gregory shortened October of A.D. 1582 by ten days. Thursday, +October 4, 1582 (Julian) was followed immediately by Friday, October 15, +1582 (Gregorian). + +Since 0.06, we handle this correctly, so: + +adodb_mktime(0,0,0,10,15,1582) - adodb_mktime(0,0,0,10,4,1582) + == 24 * 3600 (1 day) + +============================================================================= + +COPYRIGHT + +(c) 2003-2014 John Lim and released under BSD-style license except for code by +jackbbs, which includes adodb_mktime, adodb_get_gmt_diff, adodb_is_leap_year +and originally found at http://www.php.net/manual/en/function.mktime.php + +============================================================================= + +BUG REPORTS + +These should be posted to the ADOdb forums at + + http://phplens.com/lens/lensforum/topics.php?id=4 + +============================================================================= + +FUNCTION DESCRIPTIONS + +** FUNCTION adodb_time() + +Returns the current time measured in the number of seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) as an unsigned integer. + +** FUNCTION adodb_getdate($date=false) + +Returns an array containing date information, as getdate(), but supports +dates greater than 1901 to 2038. The local date/time format is derived from a +heuristic the first time adodb_getdate is called. + + +** FUNCTION adodb_date($fmt, $timestamp = false) + +Convert a timestamp to a formatted local date. If $timestamp is not defined, the +current timestamp is used. Unlike the function date(), it supports dates +outside the 1901 to 2038 range. + +The format fields that adodb_date supports: + +
+	a - "am" or "pm"
+	A - "AM" or "PM"
+	d - day of the month, 2 digits with leading zeros; i.e. "01" to "31"
+	D - day of the week, textual, 3 letters; e.g. "Fri"
+	F - month, textual, long; e.g. "January"
+	g - hour, 12-hour format without leading zeros; i.e. "1" to "12"
+	G - hour, 24-hour format without leading zeros; i.e. "0" to "23"
+	h - hour, 12-hour format; i.e. "01" to "12"
+	H - hour, 24-hour format; i.e. "00" to "23"
+	i - minutes; i.e. "00" to "59"
+	j - day of the month without leading zeros; i.e. "1" to "31"
+	l (lowercase 'L') - day of the week, textual, long; e.g. "Friday"
+	L - boolean for whether it is a leap year; i.e. "0" or "1"
+	m - month; i.e. "01" to "12"
+	M - month, textual, 3 letters; e.g. "Jan"
+	n - month without leading zeros; i.e. "1" to "12"
+	O - Difference to Greenwich time in hours; e.g. "+0200"
+	Q - Quarter, as in 1, 2, 3, 4
+	r - RFC 2822 formatted date; e.g. "Thu, 21 Dec 2000 16:01:07 +0200"
+	s - seconds; i.e. "00" to "59"
+	S - English ordinal suffix for the day of the month, 2 characters;
+	   			i.e. "st", "nd", "rd" or "th"
+	t - number of days in the given month; i.e. "28" to "31"
+	T - Timezone setting of this machine; e.g. "EST" or "MDT"
+	U - seconds since the Unix Epoch (January 1 1970 00:00:00 GMT)
+	w - day of the week, numeric, i.e. "0" (Sunday) to "6" (Saturday)
+	Y - year, 4 digits; e.g. "1999"
+	y - year, 2 digits; e.g. "99"
+	z - day of the year; i.e. "0" to "365"
+	Z - timezone offset in seconds (i.e. "-43200" to "43200").
+	   			The offset for timezones west of UTC is always negative,
+				and for those east of UTC is always positive.
+
+ +Unsupported: +
+	B - Swatch Internet time
+	I (capital i) - "1" if Daylight Savings Time, "0" otherwise.
+	W - ISO-8601 week number of year, weeks starting on Monday
+
+
+ + +** FUNCTION adodb_date2($fmt, $isoDateString = false) +Same as adodb_date, but 2nd parameter accepts iso date, eg. + + adodb_date2('d-M-Y H:i','2003-12-25 13:01:34'); + + +** FUNCTION adodb_gmdate($fmt, $timestamp = false) + +Convert a timestamp to a formatted GMT date. If $timestamp is not defined, the +current timestamp is used. Unlike the function date(), it supports dates +outside the 1901 to 2038 range. + + +** FUNCTION adodb_mktime($hr, $min, $sec[, $month, $day, $year]) + +Converts a local date to a unix timestamp. Unlike the function mktime(), it supports +dates outside the 1901 to 2038 range. All parameters are optional. + + +** FUNCTION adodb_gmmktime($hr, $min, $sec [, $month, $day, $year]) + +Converts a gmt date to a unix timestamp. Unlike the function gmmktime(), it supports +dates outside the 1901 to 2038 range. Differs from gmmktime() in that all parameters +are currently compulsory. + +** FUNCTION adodb_gmstrftime($fmt, $timestamp = false) +Convert a timestamp to a formatted GMT date. + +** FUNCTION adodb_strftime($fmt, $timestamp = false) + +Convert a timestamp to a formatted local date. Internally converts $fmt into +adodb_date format, then echo result. + +For best results, you can define the local date format yourself. Define a global +variable $ADODB_DATE_LOCALE which is an array, 1st element is date format using +adodb_date syntax, and 2nd element is the time format, also in adodb_date syntax. + + eg. $ADODB_DATE_LOCALE = array('d/m/Y','H:i:s'); + + Supported format codes: + +
+	%a - abbreviated weekday name according to the current locale
+	%A - full weekday name according to the current locale
+	%b - abbreviated month name according to the current locale
+	%B - full month name according to the current locale
+	%c - preferred date and time representation for the current locale
+	%d - day of the month as a decimal number (range 01 to 31)
+	%D - same as %m/%d/%y
+	%e - day of the month as a decimal number, a single digit is preceded by a space (range ' 1' to '31')
+	%h - same as %b
+	%H - hour as a decimal number using a 24-hour clock (range 00 to 23)
+	%I - hour as a decimal number using a 12-hour clock (range 01 to 12)
+	%m - month as a decimal number (range 01 to 12)
+	%M - minute as a decimal number
+	%n - newline character
+	%p - either `am' or `pm' according to the given time value, or the corresponding strings for the current locale
+	%r - time in a.m. and p.m. notation
+	%R - time in 24 hour notation
+	%S - second as a decimal number
+	%t - tab character
+	%T - current time, equal to %H:%M:%S
+	%x - preferred date representation for the current locale without the time
+	%X - preferred time representation for the current locale without the date
+	%y - year as a decimal number without a century (range 00 to 99)
+	%Y - year as a decimal number including the century
+	%Z - time zone or name or abbreviation
+	%% - a literal `%' character
+
+ + Unsupported codes: +
+	%C - century number (the year divided by 100 and truncated to an integer, range 00 to 99)
+	%g - like %G, but without the century.
+	%G - The 4-digit year corresponding to the ISO week number (see %V).
+	     This has the same format and value as %Y, except that if the ISO week number belongs
+		 to the previous or next year, that year is used instead.
+	%j - day of the year as a decimal number (range 001 to 366)
+	%u - weekday as a decimal number [1,7], with 1 representing Monday
+	%U - week number of the current year as a decimal number, starting
+	    with the first Sunday as the first day of the first week
+	%V - The ISO 8601:1988 week number of the current year as a decimal number,
+	     range 01 to 53, where week 1 is the first week that has at least 4 days in the
+		 current year, and with Monday as the first day of the week. (Use %G or %g for
+		 the year component that corresponds to the week number for the specified timestamp.)
+	%w - day of the week as a decimal, Sunday being 0
+	%W - week number of the current year as a decimal number, starting with the
+	     first Monday as the first day of the first week
+
+ +============================================================================= + +NOTES + +Useful url for generating test timestamps: + http://www.4webhelp.net/us/timestamp.php + +Possible future optimizations include + +a. Using an algorithm similar to Plauger's in "The Standard C Library" +(page 428, xttotm.c _Ttotm() function). Plauger's algorithm will not +work outside 32-bit signed range, so i decided not to implement it. + +b. Implement daylight savings, which looks awfully complicated, see + http://webexhibits.org/daylightsaving/ + + +CHANGELOG +- 16 Jan 2011 0.36 +Added adodb_time() which returns current time. If > 2038, will return as float + +- 7 Feb 2011 0.35 +Changed adodb_date to be symmetric with adodb_mktime. See $jan1_71. fix for bc. + +- 13 July 2010 0.34 +Changed adodb_get_gm_diff to use DateTimeZone(). + +- 11 Feb 2008 0.33 +* Bug in 0.32 fix for hour handling. Fixed. + +- 1 Feb 2008 0.32 +* Now adodb_mktime(0,0,0,12+$m,20,2040) works properly. + +- 10 Jan 2008 0.31 +* Now adodb_mktime(0,0,0,24,1,2037) works correctly. + +- 15 July 2007 0.30 +Added PHP 5.2.0 compatability fixes. + * gmtime behaviour for 1970 has changed. We use the actual date if it is between 1970 to 2038 to get the + * timezone, otherwise we use the current year as the baseline to retrieve the timezone. + * Also the timezone's in php 5.2.* support historical data better, eg. if timezone today was +8, but + in 1970 it was +7:30, then php 5.2 return +7:30, while this library will use +8. + * + +- 19 March 2006 0.24 +Changed strftime() locale detection, because some locales prepend the day of week to the date when %c is used. + +- 10 Feb 2006 0.23 +PHP5 compat: when we detect PHP5, the RFC2822 format for gmt 0000hrs is changed from -0000 to +0000. + In PHP4, we will still use -0000 for 100% compat with PHP4. + +- 08 Sept 2005 0.22 +In adodb_date2(), $is_gmt not supported properly. Fixed. + +- 18 July 2005 0.21 +In PHP 4.3.11, the 'r' format has changed. Leading 0 in day is added. Changed for compat. +Added support for negative months in adodb_mktime(). + +- 24 Feb 2005 0.20 +Added limited strftime/gmstrftime support. x10 improvement in performance of adodb_date(). + +- 21 Dec 2004 0.17 +In adodb_getdate(), the timestamp was accidentally converted to gmt when $is_gmt is false. +Also adodb_mktime(0,0,0) did not work properly. Both fixed thx Mauro. + +- 17 Nov 2004 0.16 +Removed intval typecast in adodb_mktime() for secs, allowing: + adodb_mktime(0,0,0 + 2236672153,1,1,1934); +Suggested by Ryan. + +- 18 July 2004 0.15 +All params in adodb_mktime were formerly compulsory. Now only the hour, min, secs is compulsory. +This brings it more in line with mktime (still not identical). + +- 23 June 2004 0.14 + +Allow you to define your own daylights savings function, adodb_daylight_sv. +If the function is defined (somewhere in an include), then you can correct for daylights savings. + +In this example, we apply daylights savings in June or July, adding one hour. This is extremely +unrealistic as it does not take into account time-zone, geographic location, current year. + +function adodb_daylight_sv(&$arr, $is_gmt) +{ + if ($is_gmt) return; + $m = $arr['mon']; + if ($m == 6 || $m == 7) $arr['hours'] += 1; +} + +This is only called by adodb_date() and not by adodb_mktime(). + +The format of $arr is +Array ( + [seconds] => 0 + [minutes] => 0 + [hours] => 0 + [mday] => 1 # day of month, eg 1st day of the month + [mon] => 2 # month (eg. Feb) + [year] => 2102 + [yday] => 31 # days in current year + [leap] => # true if leap year + [ndays] => 28 # no of days in current month + ) + + +- 28 Apr 2004 0.13 +Fixed adodb_date to properly support $is_gmt. Thx to Dimitar Angelov. + +- 20 Mar 2004 0.12 +Fixed month calculation error in adodb_date. 2102-June-01 appeared as 2102-May-32. + +- 26 Oct 2003 0.11 +Because of daylight savings problems (some systems apply daylight savings to +January!!!), changed adodb_get_gmt_diff() to ignore daylight savings. + +- 9 Aug 2003 0.10 +Fixed bug with dates after 2038. +See http://phplens.com/lens/lensforum/msgs.php?id=6980 + +- 1 July 2003 0.09 +Added support for Q (Quarter). +Added adodb_date2(), which accepts ISO date in 2nd param + +- 3 March 2003 0.08 +Added support for 'S' adodb_date() format char. Added constant ADODB_ALLOW_NEGATIVE_TS +if you want PHP to handle negative timestamps between 1901 to 1969. + +- 27 Feb 2003 0.07 +All negative numbers handled by adodb now because of RH 7.3+ problems. +See http://bugs.php.net/bug.php?id=20048&edit=2 + +- 4 Feb 2003 0.06 +Fixed a typo, 1852 changed to 1582! This means that pre-1852 dates +are now correctly handled. + +- 29 Jan 2003 0.05 + +Leap year checking differs under Julian calendar (pre 1582). Also +leap year code optimized by checking for most common case first. + +We also handle month overflow correctly in mktime (eg month set to 13). + +Day overflow for less than one month's days is supported. + +- 28 Jan 2003 0.04 + +Gregorian correction handled. In PHP5, we might throw an error if +mktime uses invalid dates around 5-14 Oct 1582. Released with ADOdb 3.10. +Added limbo 5-14 Oct 1582 check, when we set to 15 Oct 1582. + +- 27 Jan 2003 0.03 + +Fixed some more month problems due to gmt issues. Added constant ADODB_DATE_VERSION. +Fixed calculation of days since start of year for <1970. + +- 27 Jan 2003 0.02 + +Changed _adodb_getdate() to inline leap year checking for better performance. +Fixed problem with time-zones west of GMT +0000. + +- 24 Jan 2003 0.01 + +First implementation. +*/ + + +/* Initialization */ + +/* + Version Number +*/ +define('ADODB_DATE_VERSION',0.35); + +$ADODB_DATETIME_CLASS = (PHP_VERSION >= 5.2); + +/* + This code was originally for windows. But apparently this problem happens + also with Linux, RH 7.3 and later! + + glibc-2.2.5-34 and greater has been changed to return -1 for dates < + 1970. This used to work. The problem exists with RedHat 7.3 and 8.0 + echo (mktime(0, 0, 0, 1, 1, 1960)); // prints -1 + + References: + http://bugs.php.net/bug.php?id=20048&edit=2 + http://lists.debian.org/debian-glibc/2002/debian-glibc-200205/msg00010.html +*/ + +if (!defined('ADODB_ALLOW_NEGATIVE_TS')) define('ADODB_NO_NEGATIVE_TS',1); + +function adodb_date_test_date($y1,$m,$d=13) +{ + $h = round(rand()% 24); + $t = adodb_mktime($h,0,0,$m,$d,$y1); + $rez = adodb_date('Y-n-j H:i:s',$t); + if ($h == 0) $h = '00'; + else if ($h < 10) $h = '0'.$h; + if ("$y1-$m-$d $h:00:00" != $rez) { + print "$y1 error, expected=$y1-$m-$d $h:00:00, adodb=$rez
"; + return false; + } + return true; +} + +function adodb_date_test_strftime($fmt) +{ + $s1 = strftime($fmt); + $s2 = adodb_strftime($fmt); + + if ($s1 == $s2) return true; + + echo "error for $fmt, strftime=$s1, adodb=$s2
"; + return false; +} + +/** + Test Suite +*/ +function adodb_date_test() +{ + + for ($m=-24; $m<=24; $m++) + echo "$m :",adodb_date('d-m-Y',adodb_mktime(0,0,0,1+$m,20,2040)),"
"; + + error_reporting(E_ALL); + print "

Testing adodb_date and adodb_mktime. version=".ADODB_DATE_VERSION.' PHP='.PHP_VERSION."

"; + @set_time_limit(0); + $fail = false; + + // This flag disables calling of PHP native functions, so we can properly test the code + if (!defined('ADODB_TEST_DATES')) define('ADODB_TEST_DATES',1); + + $t = time(); + + + $fmt = 'Y-m-d H:i:s'; + echo '
';
+	echo 'adodb: ',adodb_date($fmt,$t),'
'; + echo 'php : ',date($fmt,$t),'
'; + echo '
'; + + adodb_date_test_strftime('%Y %m %x %X'); + adodb_date_test_strftime("%A %d %B %Y"); + adodb_date_test_strftime("%H %M S"); + + $t = adodb_mktime(0,0,0); + if (!(adodb_date('Y-m-d') == date('Y-m-d'))) print 'Error in '.adodb_mktime(0,0,0).'
'; + + $t = adodb_mktime(0,0,0,6,1,2102); + if (!(adodb_date('Y-m-d',$t) == '2102-06-01')) print 'Error in '.adodb_date('Y-m-d',$t).'
'; + + $t = adodb_mktime(0,0,0,2,1,2102); + if (!(adodb_date('Y-m-d',$t) == '2102-02-01')) print 'Error in '.adodb_date('Y-m-d',$t).'
'; + + + print "

Testing gregorian <=> julian conversion

"; + $t = adodb_mktime(0,0,0,10,11,1492); + //http://www.holidayorigins.com/html/columbus_day.html - Friday check + if (!(adodb_date('D Y-m-d',$t) == 'Fri 1492-10-11')) print 'Error in Columbus landing
'; + + $t = adodb_mktime(0,0,0,2,29,1500); + if (!(adodb_date('Y-m-d',$t) == '1500-02-29')) print 'Error in julian leap years
'; + + $t = adodb_mktime(0,0,0,2,29,1700); + if (!(adodb_date('Y-m-d',$t) == '1700-03-01')) print 'Error in gregorian leap years
'; + + print adodb_mktime(0,0,0,10,4,1582).' '; + print adodb_mktime(0,0,0,10,15,1582); + $diff = (adodb_mktime(0,0,0,10,15,1582) - adodb_mktime(0,0,0,10,4,1582)); + if ($diff != 3600*24) print " Error in gregorian correction = ".($diff/3600/24)." days
"; + + print " 15 Oct 1582, Fri=".(adodb_dow(1582,10,15) == 5 ? 'Fri' : 'Error')."
"; + print " 4 Oct 1582, Thu=".(adodb_dow(1582,10,4) == 4 ? 'Thu' : 'Error')."
"; + + print "

Testing overflow

"; + + $t = adodb_mktime(0,0,0,3,33,1965); + if (!(adodb_date('Y-m-d',$t) == '1965-04-02')) print 'Error in day overflow 1
'; + $t = adodb_mktime(0,0,0,4,33,1971); + if (!(adodb_date('Y-m-d',$t) == '1971-05-03')) print 'Error in day overflow 2
'; + $t = adodb_mktime(0,0,0,1,60,1965); + if (!(adodb_date('Y-m-d',$t) == '1965-03-01')) print 'Error in day overflow 3 '.adodb_date('Y-m-d',$t).'
'; + $t = adodb_mktime(0,0,0,12,32,1965); + if (!(adodb_date('Y-m-d',$t) == '1966-01-01')) print 'Error in day overflow 4 '.adodb_date('Y-m-d',$t).'
'; + $t = adodb_mktime(0,0,0,12,63,1965); + if (!(adodb_date('Y-m-d',$t) == '1966-02-01')) print 'Error in day overflow 5 '.adodb_date('Y-m-d',$t).'
'; + $t = adodb_mktime(0,0,0,13,3,1965); + if (!(adodb_date('Y-m-d',$t) == '1966-01-03')) print 'Error in mth overflow 1
'; + + print "Testing 2-digit => 4-digit year conversion

"; + if (adodb_year_digit_check(00) != 2000) print "Err 2-digit 2000
"; + if (adodb_year_digit_check(10) != 2010) print "Err 2-digit 2010
"; + if (adodb_year_digit_check(20) != 2020) print "Err 2-digit 2020
"; + if (adodb_year_digit_check(30) != 2030) print "Err 2-digit 2030
"; + if (adodb_year_digit_check(40) != 1940) print "Err 2-digit 1940
"; + if (adodb_year_digit_check(50) != 1950) print "Err 2-digit 1950
"; + if (adodb_year_digit_check(90) != 1990) print "Err 2-digit 1990
"; + + // Test string formating + print "

Testing date formating

"; + + $fmt = '\d\a\t\e T Y-m-d H:i:s a A d D F g G h H i j l L m M n O \R\F\C2822 r s t U w y Y z Z 2003'; + $s1 = date($fmt,0); + $s2 = adodb_date($fmt,0); + if ($s1 != $s2) { + print " date() 0 failed
$s1
$s2
"; + } + flush(); + for ($i=100; --$i > 0; ) { + + $ts = 3600.0*((rand()%60000)+(rand()%60000))+(rand()%60000); + $s1 = date($fmt,$ts); + $s2 = adodb_date($fmt,$ts); + //print "$s1
$s2

"; + $pos = strcmp($s1,$s2); + + if (($s1) != ($s2)) { + for ($j=0,$k=strlen($s1); $j < $k; $j++) { + if ($s1[$j] != $s2[$j]) { + print substr($s1,$j).' '; + break; + } + } + print "Error date(): $ts

+  \"$s1\" (date len=".strlen($s1).")
+  \"$s2\" (adodb_date len=".strlen($s2).")

"; + $fail = true; + } + + $a1 = getdate($ts); + $a2 = adodb_getdate($ts); + $rez = array_diff($a1,$a2); + if (sizeof($rez)>0) { + print "Error getdate() $ts
"; + print_r($a1); + print "
"; + print_r($a2); + print "

"; + $fail = true; + } + } + + // Test generation of dates outside 1901-2038 + print "

Testing random dates between 100 and 4000

"; + adodb_date_test_date(100,1); + for ($i=100; --$i >= 0;) { + $y1 = 100+rand(0,1970-100); + $m = rand(1,12); + adodb_date_test_date($y1,$m); + + $y1 = 3000-rand(0,3000-1970); + adodb_date_test_date($y1,$m); + } + print '

'; + $start = 1960+rand(0,10); + $yrs = 12; + $i = 365.25*86400*($start-1970); + $offset = 36000+rand(10000,60000); + $max = 365*$yrs*86400; + $lastyear = 0; + + // we generate a timestamp, convert it to a date, and convert it back to a timestamp + // and check if the roundtrip broke the original timestamp value. + print "Testing $start to ".($start+$yrs).", or $max seconds, offset=$offset: "; + $cnt = 0; + for ($max += $i; $i < $max; $i += $offset) { + $ret = adodb_date('m,d,Y,H,i,s',$i); + $arr = explode(',',$ret); + if ($lastyear != $arr[2]) { + $lastyear = $arr[2]; + print " $lastyear "; + flush(); + } + $newi = adodb_mktime($arr[3],$arr[4],$arr[5],$arr[0],$arr[1],$arr[2]); + if ($i != $newi) { + print "Error at $i, adodb_mktime returned $newi ($ret)"; + $fail = true; + break; + } + $cnt += 1; + } + echo "Tested $cnt dates
"; + if (!$fail) print "

Passed !

"; + else print "

Failed :-(

"; +} + +function adodb_time() +{ + $d = new DateTime(); + return $d->format('U'); +} + +/** + Returns day of week, 0 = Sunday,... 6=Saturday. + Algorithm from PEAR::Date_Calc +*/ +function adodb_dow($year, $month, $day) +{ +/* +Pope Gregory removed 10 days - October 5 to October 14 - from the year 1582 and +proclaimed that from that time onwards 3 days would be dropped from the calendar +every 400 years. + +Thursday, October 4, 1582 (Julian) was followed immediately by Friday, October 15, 1582 (Gregorian). +*/ + if ($year <= 1582) { + if ($year < 1582 || + ($year == 1582 && ($month < 10 || ($month == 10 && $day < 15)))) $greg_correction = 3; + else + $greg_correction = 0; + } else + $greg_correction = 0; + + if($month > 2) + $month -= 2; + else { + $month += 10; + $year--; + } + + $day = floor((13 * $month - 1) / 5) + + $day + ($year % 100) + + floor(($year % 100) / 4) + + floor(($year / 100) / 4) - 2 * + floor($year / 100) + 77 + $greg_correction; + + return $day - 7 * floor($day / 7); +} + + +/** + Checks for leap year, returns true if it is. No 2-digit year check. Also + handles julian calendar correctly. +*/ +function _adodb_is_leap_year($year) +{ + if ($year % 4 != 0) return false; + + if ($year % 400 == 0) { + return true; + // if gregorian calendar (>1582), century not-divisible by 400 is not leap + } else if ($year > 1582 && $year % 100 == 0 ) { + return false; + } + + return true; +} + + +/** + checks for leap year, returns true if it is. Has 2-digit year check +*/ +function adodb_is_leap_year($year) +{ + return _adodb_is_leap_year(adodb_year_digit_check($year)); +} + +/** + Fix 2-digit years. Works for any century. + Assumes that if 2-digit is more than 30 years in future, then previous century. +*/ +function adodb_year_digit_check($y) +{ + if ($y < 100) { + + $yr = (integer) date("Y"); + $century = (integer) ($yr /100); + + if ($yr%100 > 50) { + $c1 = $century + 1; + $c0 = $century; + } else { + $c1 = $century; + $c0 = $century - 1; + } + $c1 *= 100; + // if 2-digit year is less than 30 years in future, set it to this century + // otherwise if more than 30 years in future, then we set 2-digit year to the prev century. + if (($y + $c1) < $yr+30) $y = $y + $c1; + else $y = $y + $c0*100; + } + return $y; +} + +function adodb_get_gmt_diff_ts($ts) +{ + if (0 <= $ts && $ts <= 0x7FFFFFFF) { // check if number in 32-bit signed range) { + $arr = getdate($ts); + $y = $arr['year']; + $m = $arr['mon']; + $d = $arr['mday']; + return adodb_get_gmt_diff($y,$m,$d); + } else { + return adodb_get_gmt_diff(false,false,false); + } + +} + +/** + get local time zone offset from GMT. Does not handle historical timezones before 1970. +*/ +function adodb_get_gmt_diff($y,$m,$d) +{ +static $TZ,$tzo; +global $ADODB_DATETIME_CLASS; + + if (!defined('ADODB_TEST_DATES')) $y = false; + else if ($y < 1970 || $y >= 2038) $y = false; + + if ($ADODB_DATETIME_CLASS && $y !== false) { + $dt = new DateTime(); + $dt->setISODate($y,$m,$d); + if (empty($tzo)) { + $tzo = new DateTimeZone(date_default_timezone_get()); + # $tzt = timezone_transitions_get( $tzo ); + } + return -$tzo->getOffset($dt); + } else { + if (isset($TZ)) return $TZ; + $y = date('Y'); + /* + if (function_exists('date_default_timezone_get') && function_exists('timezone_offset_get')) { + $tzonename = date_default_timezone_get(); + if ($tzonename) { + $tobj = new DateTimeZone($tzonename); + $TZ = -timezone_offset_get($tobj,new DateTime("now",$tzo)); + } + } + */ + if (empty($TZ)) $TZ = mktime(0,0,0,12,2,$y) - gmmktime(0,0,0,12,2,$y); + } + return $TZ; +} + +/** + Returns an array with date info. +*/ +function adodb_getdate($d=false,$fast=false) +{ + if ($d === false) return getdate(); + if (!defined('ADODB_TEST_DATES')) { + if ((abs($d) <= 0x7FFFFFFF)) { // check if number in 32-bit signed range + if (!defined('ADODB_NO_NEGATIVE_TS') || $d >= 0) // if windows, must be +ve integer + return @getdate($d); + } + } + return _adodb_getdate($d); +} + +/* +// generate $YRS table for _adodb_getdate() +function adodb_date_gentable($out=true) +{ + + for ($i=1970; $i >= 1600; $i-=10) { + $s = adodb_gmmktime(0,0,0,1,1,$i); + echo "$i => $s,
"; + } +} +adodb_date_gentable(); + +for ($i=1970; $i > 1500; $i--) { + +echo "
$i "; + adodb_date_test_date($i,1,1); +} + +*/ + + +$_month_table_normal = array("",31,28,31,30,31,30,31,31,30,31,30,31); +$_month_table_leaf = array("",31,29,31,30,31,30,31,31,30,31,30,31); + +function adodb_validdate($y,$m,$d) +{ +global $_month_table_normal,$_month_table_leaf; + + if (_adodb_is_leap_year($y)) $marr = $_month_table_leaf; + else $marr = $_month_table_normal; + + if ($m > 12 || $m < 1) return false; + + if ($d > 31 || $d < 1) return false; + + if ($marr[$m] < $d) return false; + + if ($y < 1000 && $y > 3000) return false; + + return true; +} + +/** + Low-level function that returns the getdate() array. We have a special + $fast flag, which if set to true, will return fewer array values, + and is much faster as it does not calculate dow, etc. +*/ +function _adodb_getdate($origd=false,$fast=false,$is_gmt=false) +{ +static $YRS; +global $_month_table_normal,$_month_table_leaf; + + $d = $origd - ($is_gmt ? 0 : adodb_get_gmt_diff_ts($origd)); + $_day_power = 86400; + $_hour_power = 3600; + $_min_power = 60; + + if ($d < -12219321600) $d -= 86400*10; // if 15 Oct 1582 or earlier, gregorian correction + + $_month_table_normal = array("",31,28,31,30,31,30,31,31,30,31,30,31); + $_month_table_leaf = array("",31,29,31,30,31,30,31,31,30,31,30,31); + + $d366 = $_day_power * 366; + $d365 = $_day_power * 365; + + if ($d < 0) { + + if (empty($YRS)) $YRS = array( + 1970 => 0, + 1960 => -315619200, + 1950 => -631152000, + 1940 => -946771200, + 1930 => -1262304000, + 1920 => -1577923200, + 1910 => -1893456000, + 1900 => -2208988800, + 1890 => -2524521600, + 1880 => -2840140800, + 1870 => -3155673600, + 1860 => -3471292800, + 1850 => -3786825600, + 1840 => -4102444800, + 1830 => -4417977600, + 1820 => -4733596800, + 1810 => -5049129600, + 1800 => -5364662400, + 1790 => -5680195200, + 1780 => -5995814400, + 1770 => -6311347200, + 1760 => -6626966400, + 1750 => -6942499200, + 1740 => -7258118400, + 1730 => -7573651200, + 1720 => -7889270400, + 1710 => -8204803200, + 1700 => -8520336000, + 1690 => -8835868800, + 1680 => -9151488000, + 1670 => -9467020800, + 1660 => -9782640000, + 1650 => -10098172800, + 1640 => -10413792000, + 1630 => -10729324800, + 1620 => -11044944000, + 1610 => -11360476800, + 1600 => -11676096000); + + if ($is_gmt) $origd = $d; + // The valid range of a 32bit signed timestamp is typically from + // Fri, 13 Dec 1901 20:45:54 GMT to Tue, 19 Jan 2038 03:14:07 GMT + // + + # old algorithm iterates through all years. new algorithm does it in + # 10 year blocks + + /* + # old algo + for ($a = 1970 ; --$a >= 0;) { + $lastd = $d; + + if ($leaf = _adodb_is_leap_year($a)) $d += $d366; + else $d += $d365; + + if ($d >= 0) { + $year = $a; + break; + } + } + */ + + $lastsecs = 0; + $lastyear = 1970; + foreach($YRS as $year => $secs) { + if ($d >= $secs) { + $a = $lastyear; + break; + } + $lastsecs = $secs; + $lastyear = $year; + } + + $d -= $lastsecs; + if (!isset($a)) $a = $lastyear; + + //echo ' yr=',$a,' ', $d,'.'; + + for (; --$a >= 0;) { + $lastd = $d; + + if ($leaf = _adodb_is_leap_year($a)) $d += $d366; + else $d += $d365; + + if ($d >= 0) { + $year = $a; + break; + } + } + /**/ + + $secsInYear = 86400 * ($leaf ? 366 : 365) + $lastd; + + $d = $lastd; + $mtab = ($leaf) ? $_month_table_leaf : $_month_table_normal; + for ($a = 13 ; --$a > 0;) { + $lastd = $d; + $d += $mtab[$a] * $_day_power; + if ($d >= 0) { + $month = $a; + $ndays = $mtab[$a]; + break; + } + } + + $d = $lastd; + $day = $ndays + ceil(($d+1) / ($_day_power)); + + $d += ($ndays - $day+1)* $_day_power; + $hour = floor($d/$_hour_power); + + } else { + for ($a = 1970 ;; $a++) { + $lastd = $d; + + if ($leaf = _adodb_is_leap_year($a)) $d -= $d366; + else $d -= $d365; + if ($d < 0) { + $year = $a; + break; + } + } + $secsInYear = $lastd; + $d = $lastd; + $mtab = ($leaf) ? $_month_table_leaf : $_month_table_normal; + for ($a = 1 ; $a <= 12; $a++) { + $lastd = $d; + $d -= $mtab[$a] * $_day_power; + if ($d < 0) { + $month = $a; + $ndays = $mtab[$a]; + break; + } + } + $d = $lastd; + $day = ceil(($d+1) / $_day_power); + $d = $d - ($day-1) * $_day_power; + $hour = floor($d /$_hour_power); + } + + $d -= $hour * $_hour_power; + $min = floor($d/$_min_power); + $secs = $d - $min * $_min_power; + if ($fast) { + return array( + 'seconds' => $secs, + 'minutes' => $min, + 'hours' => $hour, + 'mday' => $day, + 'mon' => $month, + 'year' => $year, + 'yday' => floor($secsInYear/$_day_power), + 'leap' => $leaf, + 'ndays' => $ndays + ); + } + + + $dow = adodb_dow($year,$month,$day); + + return array( + 'seconds' => $secs, + 'minutes' => $min, + 'hours' => $hour, + 'mday' => $day, + 'wday' => $dow, + 'mon' => $month, + 'year' => $year, + 'yday' => floor($secsInYear/$_day_power), + 'weekday' => gmdate('l',$_day_power*(3+$dow)), + 'month' => gmdate('F',mktime(0,0,0,$month,2,1971)), + 0 => $origd + ); +} +/* + if ($isphp5) + $dates .= sprintf('%s%04d',($gmt<=0)?'+':'-',abs($gmt)/36); + else + $dates .= sprintf('%s%04d',($gmt<0)?'+':'-',abs($gmt)/36); + break;*/ +function adodb_tz_offset($gmt,$isphp5) +{ + $zhrs = abs($gmt)/3600; + $hrs = floor($zhrs); + if ($isphp5) + return sprintf('%s%02d%02d',($gmt<=0)?'+':'-',floor($zhrs),($zhrs-$hrs)*60); + else + return sprintf('%s%02d%02d',($gmt<0)?'+':'-',floor($zhrs),($zhrs-$hrs)*60); +} + + +function adodb_gmdate($fmt,$d=false) +{ + return adodb_date($fmt,$d,true); +} + +// accepts unix timestamp and iso date format in $d +function adodb_date2($fmt, $d=false, $is_gmt=false) +{ + if ($d !== false) { + if (!preg_match( + "|^([0-9]{4})[-/\.]?([0-9]{1,2})[-/\.]?([0-9]{1,2})[ -]?(([0-9]{1,2}):?([0-9]{1,2}):?([0-9\.]{1,4}))?|", + ($d), $rr)) return adodb_date($fmt,false,$is_gmt); + + if ($rr[1] <= 100 && $rr[2]<= 1) return adodb_date($fmt,false,$is_gmt); + + // h-m-s-MM-DD-YY + if (!isset($rr[5])) $d = adodb_mktime(0,0,0,$rr[2],$rr[3],$rr[1],false,$is_gmt); + else $d = @adodb_mktime($rr[5],$rr[6],$rr[7],$rr[2],$rr[3],$rr[1],false,$is_gmt); + } + + return adodb_date($fmt,$d,$is_gmt); +} + + +/** + Return formatted date based on timestamp $d +*/ +function adodb_date($fmt,$d=false,$is_gmt=false) +{ +static $daylight; +global $ADODB_DATETIME_CLASS; +static $jan1_1971; + + + if (!isset($daylight)) { + $daylight = function_exists('adodb_daylight_sv'); + if (empty($jan1_1971)) $jan1_1971 = mktime(0,0,0,1,1,1971); // we only use date() when > 1970 as adodb_mktime() only uses mktime() when > 1970 + } + + if ($d === false) return ($is_gmt)? @gmdate($fmt): @date($fmt); + if (!defined('ADODB_TEST_DATES')) { + if ((abs($d) <= 0x7FFFFFFF)) { // check if number in 32-bit signed range + + if (!defined('ADODB_NO_NEGATIVE_TS') || $d >= $jan1_1971) // if windows, must be +ve integer + return ($is_gmt)? @gmdate($fmt,$d): @date($fmt,$d); + + } + } + $_day_power = 86400; + + $arr = _adodb_getdate($d,true,$is_gmt); + + if ($daylight) adodb_daylight_sv($arr, $is_gmt); + + $year = $arr['year']; + $month = $arr['mon']; + $day = $arr['mday']; + $hour = $arr['hours']; + $min = $arr['minutes']; + $secs = $arr['seconds']; + + $max = strlen($fmt); + $dates = ''; + + $isphp5 = PHP_VERSION >= 5; + + /* + at this point, we have the following integer vars to manipulate: + $year, $month, $day, $hour, $min, $secs + */ + for ($i=0; $i < $max; $i++) { + switch($fmt[$i]) { + case 'e': + $dates .= date('e'); + break; + case 'T': + if ($ADODB_DATETIME_CLASS) { + $dt = new DateTime(); + $dt->SetDate($year,$month,$day); + $dates .= $dt->Format('T'); + } else + $dates .= date('T'); + break; + // YEAR + case 'L': $dates .= $arr['leap'] ? '1' : '0'; break; + case 'r': // Thu, 21 Dec 2000 16:01:07 +0200 + + // 4.3.11 uses '04 Jun 2004' + // 4.3.8 uses ' 4 Jun 2004' + $dates .= gmdate('D',$_day_power*(3+adodb_dow($year,$month,$day))).', ' + . ($day<10?'0'.$day:$day) . ' '.date('M',mktime(0,0,0,$month,2,1971)).' '.$year.' '; + + if ($hour < 10) $dates .= '0'.$hour; else $dates .= $hour; + + if ($min < 10) $dates .= ':0'.$min; else $dates .= ':'.$min; + + if ($secs < 10) $dates .= ':0'.$secs; else $dates .= ':'.$secs; + + $gmt = adodb_get_gmt_diff($year,$month,$day); + + $dates .= ' '.adodb_tz_offset($gmt,$isphp5); + break; + + case 'Y': $dates .= $year; break; + case 'y': $dates .= substr($year,strlen($year)-2,2); break; + // MONTH + case 'm': if ($month<10) $dates .= '0'.$month; else $dates .= $month; break; + case 'Q': $dates .= ($month+3)>>2; break; + case 'n': $dates .= $month; break; + case 'M': $dates .= date('M',mktime(0,0,0,$month,2,1971)); break; + case 'F': $dates .= date('F',mktime(0,0,0,$month,2,1971)); break; + // DAY + case 't': $dates .= $arr['ndays']; break; + case 'z': $dates .= $arr['yday']; break; + case 'w': $dates .= adodb_dow($year,$month,$day); break; + case 'l': $dates .= gmdate('l',$_day_power*(3+adodb_dow($year,$month,$day))); break; + case 'D': $dates .= gmdate('D',$_day_power*(3+adodb_dow($year,$month,$day))); break; + case 'j': $dates .= $day; break; + case 'd': if ($day<10) $dates .= '0'.$day; else $dates .= $day; break; + case 'S': + $d10 = $day % 10; + if ($d10 == 1) $dates .= 'st'; + else if ($d10 == 2 && $day != 12) $dates .= 'nd'; + else if ($d10 == 3) $dates .= 'rd'; + else $dates .= 'th'; + break; + + // HOUR + case 'Z': + $dates .= ($is_gmt) ? 0 : -adodb_get_gmt_diff($year,$month,$day); break; + case 'O': + $gmt = ($is_gmt) ? 0 : adodb_get_gmt_diff($year,$month,$day); + + $dates .= adodb_tz_offset($gmt,$isphp5); + break; + + case 'H': + if ($hour < 10) $dates .= '0'.$hour; + else $dates .= $hour; + break; + case 'h': + if ($hour > 12) $hh = $hour - 12; + else { + if ($hour == 0) $hh = '12'; + else $hh = $hour; + } + + if ($hh < 10) $dates .= '0'.$hh; + else $dates .= $hh; + break; + + case 'G': + $dates .= $hour; + break; + + case 'g': + if ($hour > 12) $hh = $hour - 12; + else { + if ($hour == 0) $hh = '12'; + else $hh = $hour; + } + $dates .= $hh; + break; + // MINUTES + case 'i': if ($min < 10) $dates .= '0'.$min; else $dates .= $min; break; + // SECONDS + case 'U': $dates .= $d; break; + case 's': if ($secs < 10) $dates .= '0'.$secs; else $dates .= $secs; break; + // AM/PM + // Note 00:00 to 11:59 is AM, while 12:00 to 23:59 is PM + case 'a': + if ($hour>=12) $dates .= 'pm'; + else $dates .= 'am'; + break; + case 'A': + if ($hour>=12) $dates .= 'PM'; + else $dates .= 'AM'; + break; + default: + $dates .= $fmt[$i]; break; + // ESCAPE + case "\\": + $i++; + if ($i < $max) $dates .= $fmt[$i]; + break; + } + } + return $dates; +} + +/** + Returns a timestamp given a GMT/UTC time. + Note that $is_dst is not implemented and is ignored. +*/ +function adodb_gmmktime($hr,$min,$sec,$mon=false,$day=false,$year=false,$is_dst=false) +{ + return adodb_mktime($hr,$min,$sec,$mon,$day,$year,$is_dst,true); +} + +/** + Return a timestamp given a local time. Originally by jackbbs. + Note that $is_dst is not implemented and is ignored. + + Not a very fast algorithm - O(n) operation. Could be optimized to O(1). +*/ +function adodb_mktime($hr,$min,$sec,$mon=false,$day=false,$year=false,$is_dst=false,$is_gmt=false) +{ + if (!defined('ADODB_TEST_DATES')) { + + if ($mon === false) { + return $is_gmt? @gmmktime($hr,$min,$sec): @mktime($hr,$min,$sec); + } + + // for windows, we don't check 1970 because with timezone differences, + // 1 Jan 1970 could generate negative timestamp, which is illegal + $usephpfns = (1970 < $year && $year < 2038 + || !defined('ADODB_NO_NEGATIVE_TS') && (1901 < $year && $year < 2038) + ); + + + if ($usephpfns && ($year + $mon/12+$day/365.25+$hr/(24*365.25) >= 2038)) $usephpfns = false; + + if ($usephpfns) { + return $is_gmt ? + @gmmktime($hr,$min,$sec,$mon,$day,$year): + @mktime($hr,$min,$sec,$mon,$day,$year); + } + } + + $gmt_different = ($is_gmt) ? 0 : adodb_get_gmt_diff($year,$mon,$day); + + /* + # disabled because some people place large values in $sec. + # however we need it for $mon because we use an array... + $hr = intval($hr); + $min = intval($min); + $sec = intval($sec); + */ + $mon = intval($mon); + $day = intval($day); + $year = intval($year); + + + $year = adodb_year_digit_check($year); + + if ($mon > 12) { + $y = floor(($mon-1)/ 12); + $year += $y; + $mon -= $y*12; + } else if ($mon < 1) { + $y = ceil((1-$mon) / 12); + $year -= $y; + $mon += $y*12; + } + + $_day_power = 86400; + $_hour_power = 3600; + $_min_power = 60; + + $_month_table_normal = array("",31,28,31,30,31,30,31,31,30,31,30,31); + $_month_table_leaf = array("",31,29,31,30,31,30,31,31,30,31,30,31); + + $_total_date = 0; + if ($year >= 1970) { + for ($a = 1970 ; $a <= $year; $a++) { + $leaf = _adodb_is_leap_year($a); + if ($leaf == true) { + $loop_table = $_month_table_leaf; + $_add_date = 366; + } else { + $loop_table = $_month_table_normal; + $_add_date = 365; + } + if ($a < $year) { + $_total_date += $_add_date; + } else { + for($b=1;$b<$mon;$b++) { + $_total_date += $loop_table[$b]; + } + } + } + $_total_date +=$day-1; + $ret = $_total_date * $_day_power + $hr * $_hour_power + $min * $_min_power + $sec + $gmt_different; + + } else { + for ($a = 1969 ; $a >= $year; $a--) { + $leaf = _adodb_is_leap_year($a); + if ($leaf == true) { + $loop_table = $_month_table_leaf; + $_add_date = 366; + } else { + $loop_table = $_month_table_normal; + $_add_date = 365; + } + if ($a > $year) { $_total_date += $_add_date; + } else { + for($b=12;$b>$mon;$b--) { + $_total_date += $loop_table[$b]; + } + } + } + $_total_date += $loop_table[$mon] - $day; + + $_day_time = $hr * $_hour_power + $min * $_min_power + $sec; + $_day_time = $_day_power - $_day_time; + $ret = -( $_total_date * $_day_power + $_day_time - $gmt_different); + if ($ret < -12220185600) $ret += 10*86400; // if earlier than 5 Oct 1582 - gregorian correction + else if ($ret < -12219321600) $ret = -12219321600; // if in limbo, reset to 15 Oct 1582. + } + //print " dmy=$day/$mon/$year $hr:$min:$sec => " .$ret; + return $ret; +} + +function adodb_gmstrftime($fmt, $ts=false) +{ + return adodb_strftime($fmt,$ts,true); +} + +// hack - convert to adodb_date +function adodb_strftime($fmt, $ts=false,$is_gmt=false) +{ +global $ADODB_DATE_LOCALE; + + if (!defined('ADODB_TEST_DATES')) { + if ((abs($ts) <= 0x7FFFFFFF)) { // check if number in 32-bit signed range + if (!defined('ADODB_NO_NEGATIVE_TS') || $ts >= 0) // if windows, must be +ve integer + return ($is_gmt)? @gmstrftime($fmt,$ts): @strftime($fmt,$ts); + + } + } + + if (empty($ADODB_DATE_LOCALE)) { + /* + $tstr = strtoupper(gmstrftime('%c',31366800)); // 30 Dec 1970, 1 am + $sep = substr($tstr,2,1); + $hasAM = strrpos($tstr,'M') !== false; + */ + # see http://phplens.com/lens/lensforum/msgs.php?id=14865 for reasoning, and changelog for version 0.24 + $dstr = gmstrftime('%x',31366800); // 30 Dec 1970, 1 am + $sep = substr($dstr,2,1); + $tstr = strtoupper(gmstrftime('%X',31366800)); // 30 Dec 1970, 1 am + $hasAM = strrpos($tstr,'M') !== false; + + $ADODB_DATE_LOCALE = array(); + $ADODB_DATE_LOCALE[] = strncmp($tstr,'30',2) == 0 ? 'd'.$sep.'m'.$sep.'y' : 'm'.$sep.'d'.$sep.'y'; + $ADODB_DATE_LOCALE[] = ($hasAM) ? 'h:i:s a' : 'H:i:s'; + + } + $inpct = false; + $fmtdate = ''; + for ($i=0,$max = strlen($fmt); $i < $max; $i++) { + $ch = $fmt[$i]; + if ($ch == '%') { + if ($inpct) { + $fmtdate .= '%'; + $inpct = false; + } else + $inpct = true; + } else if ($inpct) { + + $inpct = false; + switch($ch) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 'E': + case 'O': + /* ignore format modifiers */ + $inpct = true; + break; + + case 'a': $fmtdate .= 'D'; break; + case 'A': $fmtdate .= 'l'; break; + case 'h': + case 'b': $fmtdate .= 'M'; break; + case 'B': $fmtdate .= 'F'; break; + case 'c': $fmtdate .= $ADODB_DATE_LOCALE[0].$ADODB_DATE_LOCALE[1]; break; + case 'C': $fmtdate .= '\C?'; break; // century + case 'd': $fmtdate .= 'd'; break; + case 'D': $fmtdate .= 'm/d/y'; break; + case 'e': $fmtdate .= 'j'; break; + case 'g': $fmtdate .= '\g?'; break; //? + case 'G': $fmtdate .= '\G?'; break; //? + case 'H': $fmtdate .= 'H'; break; + case 'I': $fmtdate .= 'h'; break; + case 'j': $fmtdate .= '?z'; $parsej = true; break; // wrong as j=1-based, z=0-basd + case 'm': $fmtdate .= 'm'; break; + case 'M': $fmtdate .= 'i'; break; + case 'n': $fmtdate .= "\n"; break; + case 'p': $fmtdate .= 'a'; break; + case 'r': $fmtdate .= 'h:i:s a'; break; + case 'R': $fmtdate .= 'H:i:s'; break; + case 'S': $fmtdate .= 's'; break; + case 't': $fmtdate .= "\t"; break; + case 'T': $fmtdate .= 'H:i:s'; break; + case 'u': $fmtdate .= '?u'; $parseu = true; break; // wrong strftime=1-based, date=0-based + case 'U': $fmtdate .= '?U'; $parseU = true; break;// wrong strftime=1-based, date=0-based + case 'x': $fmtdate .= $ADODB_DATE_LOCALE[0]; break; + case 'X': $fmtdate .= $ADODB_DATE_LOCALE[1]; break; + case 'w': $fmtdate .= '?w'; $parseu = true; break; // wrong strftime=1-based, date=0-based + case 'W': $fmtdate .= '?W'; $parseU = true; break;// wrong strftime=1-based, date=0-based + case 'y': $fmtdate .= 'y'; break; + case 'Y': $fmtdate .= 'Y'; break; + case 'Z': $fmtdate .= 'T'; break; + } + } else if (('A' <= ($ch) && ($ch) <= 'Z' ) || ('a' <= ($ch) && ($ch) <= 'z' )) + $fmtdate .= "\\".$ch; + else + $fmtdate .= $ch; + } + //echo "fmt=",$fmtdate,"
"; + if ($ts === false) $ts = time(); + $ret = adodb_date($fmtdate, $ts, $is_gmt); + return $ret; +} diff --git a/app/vendor/adodb/adodb-php/adodb-xmlschema.inc.php b/app/vendor/adodb/adodb-php/adodb-xmlschema.inc.php new file mode 100644 index 000000000..ca5fa5678 --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-xmlschema.inc.php @@ -0,0 +1,2224 @@ +parent = $parent; + } + + /** + * XML Callback to process start elements + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + + } + + /** + * XML Callback to process CDATA elements + * + * @access private + */ + function _tag_cdata( &$parser, $cdata ) { + + } + + /** + * XML Callback to process end elements + * + * @access private + */ + function _tag_close( &$parser, $tag ) { + + } + + function create(&$xmls) { + return array(); + } + + /** + * Destroys the object + */ + function destroy() { + } + + /** + * Checks whether the specified RDBMS is supported by the current + * database object or its ranking ancestor. + * + * @param string $platform RDBMS platform name (from ADODB platform list). + * @return boolean TRUE if RDBMS is supported; otherwise returns FALSE. + */ + function supportedPlatform( $platform = NULL ) { + return is_object( $this->parent ) ? $this->parent->supportedPlatform( $platform ) : TRUE; + } + + /** + * Returns the prefix set by the ranking ancestor of the database object. + * + * @param string $name Prefix string. + * @return string Prefix. + */ + function prefix( $name = '' ) { + return is_object( $this->parent ) ? $this->parent->prefix( $name ) : $name; + } + + /** + * Extracts a field ID from the specified field. + * + * @param string $field Field. + * @return string Field ID. + */ + function FieldID( $field ) { + return strtoupper( preg_replace( '/^`(.+)`$/', '$1', $field ) ); + } +} + +/** +* Creates a table object in ADOdb's datadict format +* +* This class stores information about a database table. As charactaristics +* of the table are loaded from the external source, methods and properties +* of this class are used to build up the table description in ADOdb's +* datadict format. +* +* @package axmls +* @access private +*/ +class dbTable extends dbObject { + + /** + * @var string Table name + */ + var $name; + + /** + * @var array Field specifier: Meta-information about each field + */ + var $fields = array(); + + /** + * @var array List of table indexes. + */ + var $indexes = array(); + + /** + * @var array Table options: Table-level options + */ + var $opts = array(); + + /** + * @var string Field index: Keeps track of which field is currently being processed + */ + var $current_field; + + /** + * @var boolean Mark table for destruction + * @access private + */ + var $drop_table; + + /** + * @var boolean Mark field for destruction (not yet implemented) + * @access private + */ + var $drop_field = array(); + + /** + * Iniitializes a new table object. + * + * @param string $prefix DB Object prefix + * @param array $attributes Array of table attributes. + */ + function __construct( &$parent, $attributes = NULL ) { + $this->parent = $parent; + $this->name = $this->prefix($attributes['NAME']); + } + + /** + * XML Callback to process start elements. Elements currently + * processed are: INDEX, DROP, FIELD, KEY, NOTNULL, AUTOINCREMENT & DEFAULT. + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + $this->currentElement = strtoupper( $tag ); + + switch( $this->currentElement ) { + case 'INDEX': + if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) { + $index = $this->addIndex( $attributes ); + xml_set_object( $parser, $index ); + } + break; + case 'DATA': + if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) { + $data = $this->addData( $attributes ); + xml_set_object( $parser, $data ); + } + break; + case 'DROP': + $this->drop(); + break; + case 'FIELD': + // Add a field + $fieldName = $attributes['NAME']; + $fieldType = $attributes['TYPE']; + $fieldSize = isset( $attributes['SIZE'] ) ? $attributes['SIZE'] : NULL; + $fieldOpts = isset( $attributes['OPTS'] ) ? $attributes['OPTS'] : NULL; + + $this->addField( $fieldName, $fieldType, $fieldSize, $fieldOpts ); + break; + case 'KEY': + case 'NOTNULL': + case 'AUTOINCREMENT': + // Add a field option + $this->addFieldOpt( $this->current_field, $this->currentElement ); + break; + case 'DEFAULT': + // Add a field option to the table object + + // Work around ADOdb datadict issue that misinterprets empty strings. + if( $attributes['VALUE'] == '' ) { + $attributes['VALUE'] = " '' "; + } + + $this->addFieldOpt( $this->current_field, $this->currentElement, $attributes['VALUE'] ); + break; + case 'DEFDATE': + case 'DEFTIMESTAMP': + // Add a field option to the table object + $this->addFieldOpt( $this->current_field, $this->currentElement ); + break; + default: + // print_r( array( $tag, $attributes ) ); + } + } + + /** + * XML Callback to process CDATA elements + * + * @access private + */ + function _tag_cdata( &$parser, $cdata ) { + switch( $this->currentElement ) { + // Table constraint + case 'CONSTRAINT': + if( isset( $this->current_field ) ) { + $this->addFieldOpt( $this->current_field, $this->currentElement, $cdata ); + } else { + $this->addTableOpt( $cdata ); + } + break; + // Table option + case 'OPT': + $this->addTableOpt( $cdata ); + break; + default: + + } + } + + /** + * XML Callback to process end elements + * + * @access private + */ + function _tag_close( &$parser, $tag ) { + $this->currentElement = ''; + + switch( strtoupper( $tag ) ) { + case 'TABLE': + $this->parent->addSQL( $this->create( $this->parent ) ); + xml_set_object( $parser, $this->parent ); + $this->destroy(); + break; + case 'FIELD': + unset($this->current_field); + break; + + } + } + + /** + * Adds an index to a table object + * + * @param array $attributes Index attributes + * @return object dbIndex object + */ + function addIndex( $attributes ) { + $name = strtoupper( $attributes['NAME'] ); + $this->indexes[$name] = new dbIndex( $this, $attributes ); + return $this->indexes[$name]; + } + + /** + * Adds data to a table object + * + * @param array $attributes Data attributes + * @return object dbData object + */ + function addData( $attributes ) { + if( !isset( $this->data ) ) { + $this->data = new dbData( $this, $attributes ); + } + return $this->data; + } + + /** + * Adds a field to a table object + * + * $name is the name of the table to which the field should be added. + * $type is an ADODB datadict field type. The following field types + * are supported as of ADODB 3.40: + * - C: varchar + * - X: CLOB (character large object) or largest varchar size + * if CLOB is not supported + * - C2: Multibyte varchar + * - X2: Multibyte CLOB + * - B: BLOB (binary large object) + * - D: Date (some databases do not support this, and we return a datetime type) + * - T: Datetime or Timestamp + * - L: Integer field suitable for storing booleans (0 or 1) + * - I: Integer (mapped to I4) + * - I1: 1-byte integer + * - I2: 2-byte integer + * - I4: 4-byte integer + * - I8: 8-byte integer + * - F: Floating point number + * - N: Numeric or decimal number + * + * @param string $name Name of the table to which the field will be added. + * @param string $type ADODB datadict field type. + * @param string $size Field size + * @param array $opts Field options array + * @return array Field specifier array + */ + function addField( $name, $type, $size = NULL, $opts = NULL ) { + $field_id = $this->FieldID( $name ); + + // Set the field index so we know where we are + $this->current_field = $field_id; + + // Set the field name (required) + $this->fields[$field_id]['NAME'] = $name; + + // Set the field type (required) + $this->fields[$field_id]['TYPE'] = $type; + + // Set the field size (optional) + if( isset( $size ) ) { + $this->fields[$field_id]['SIZE'] = $size; + } + + // Set the field options + if( isset( $opts ) ) { + $this->fields[$field_id]['OPTS'][] = $opts; + } + } + + /** + * Adds a field option to the current field specifier + * + * This method adds a field option allowed by the ADOdb datadict + * and appends it to the given field. + * + * @param string $field Field name + * @param string $opt ADOdb field option + * @param mixed $value Field option value + * @return array Field specifier array + */ + function addFieldOpt( $field, $opt, $value = NULL ) { + if( !isset( $value ) ) { + $this->fields[$this->FieldID( $field )]['OPTS'][] = $opt; + // Add the option and value + } else { + $this->fields[$this->FieldID( $field )]['OPTS'][] = array( $opt => $value ); + } + } + + /** + * Adds an option to the table + * + * This method takes a comma-separated list of table-level options + * and appends them to the table object. + * + * @param string $opt Table option + * @return array Options + */ + function addTableOpt( $opt ) { + if(isset($this->currentPlatform)) { + $this->opts[$this->parent->db->databaseType] = $opt; + } + return $this->opts; + } + + + /** + * Generates the SQL that will create the table in the database + * + * @param object $xmls adoSchema object + * @return array Array containing table creation SQL + */ + function create( &$xmls ) { + $sql = array(); + + // drop any existing indexes + if( is_array( $legacy_indexes = $xmls->dict->MetaIndexes( $this->name ) ) ) { + foreach( $legacy_indexes as $index => $index_details ) { + $sql[] = $xmls->dict->DropIndexSQL( $index, $this->name ); + } + } + + // remove fields to be dropped from table object + foreach( $this->drop_field as $field ) { + unset( $this->fields[$field] ); + } + + // if table exists + if( is_array( $legacy_fields = $xmls->dict->MetaColumns( $this->name ) ) ) { + // drop table + if( $this->drop_table ) { + $sql[] = $xmls->dict->DropTableSQL( $this->name ); + + return $sql; + } + + // drop any existing fields not in schema + foreach( $legacy_fields as $field_id => $field ) { + if( !isset( $this->fields[$field_id] ) ) { + $sql[] = $xmls->dict->DropColumnSQL( $this->name, '`'.$field->name.'`' ); + } + } + // if table doesn't exist + } else { + if( $this->drop_table ) { + return $sql; + } + + $legacy_fields = array(); + } + + // Loop through the field specifier array, building the associative array for the field options + $fldarray = array(); + + foreach( $this->fields as $field_id => $finfo ) { + // Set an empty size if it isn't supplied + if( !isset( $finfo['SIZE'] ) ) { + $finfo['SIZE'] = ''; + } + + // Initialize the field array with the type and size + $fldarray[$field_id] = array( + 'NAME' => $finfo['NAME'], + 'TYPE' => $finfo['TYPE'], + 'SIZE' => $finfo['SIZE'] + ); + + // Loop through the options array and add the field options. + if( isset( $finfo['OPTS'] ) ) { + foreach( $finfo['OPTS'] as $opt ) { + // Option has an argument. + if( is_array( $opt ) ) { + $key = key( $opt ); + $value = $opt[key( $opt )]; + @$fldarray[$field_id][$key] .= $value; + // Option doesn't have arguments + } else { + $fldarray[$field_id][$opt] = $opt; + } + } + } + } + + if( empty( $legacy_fields ) ) { + // Create the new table + $sql[] = $xmls->dict->CreateTableSQL( $this->name, $fldarray, $this->opts ); + logMsg( end( $sql ), 'Generated CreateTableSQL' ); + } else { + // Upgrade an existing table + logMsg( "Upgrading {$this->name} using '{$xmls->upgrade}'" ); + switch( $xmls->upgrade ) { + // Use ChangeTableSQL + case 'ALTER': + logMsg( 'Generated ChangeTableSQL (ALTERing table)' ); + $sql[] = $xmls->dict->ChangeTableSQL( $this->name, $fldarray, $this->opts ); + break; + case 'REPLACE': + logMsg( 'Doing upgrade REPLACE (testing)' ); + $sql[] = $xmls->dict->DropTableSQL( $this->name ); + $sql[] = $xmls->dict->CreateTableSQL( $this->name, $fldarray, $this->opts ); + break; + // ignore table + default: + return array(); + } + } + + foreach( $this->indexes as $index ) { + $sql[] = $index->create( $xmls ); + } + + if( isset( $this->data ) ) { + $sql[] = $this->data->create( $xmls ); + } + + return $sql; + } + + /** + * Marks a field or table for destruction + */ + function drop() { + if( isset( $this->current_field ) ) { + // Drop the current field + logMsg( "Dropping field '{$this->current_field}' from table '{$this->name}'" ); + // $this->drop_field[$this->current_field] = $xmls->dict->DropColumnSQL( $this->name, $this->current_field ); + $this->drop_field[$this->current_field] = $this->current_field; + } else { + // Drop the current table + logMsg( "Dropping table '{$this->name}'" ); + // $this->drop_table = $xmls->dict->DropTableSQL( $this->name ); + $this->drop_table = TRUE; + } + } +} + +/** +* Creates an index object in ADOdb's datadict format +* +* This class stores information about a database index. As charactaristics +* of the index are loaded from the external source, methods and properties +* of this class are used to build up the index description in ADOdb's +* datadict format. +* +* @package axmls +* @access private +*/ +class dbIndex extends dbObject { + + /** + * @var string Index name + */ + var $name; + + /** + * @var array Index options: Index-level options + */ + var $opts = array(); + + /** + * @var array Indexed fields: Table columns included in this index + */ + var $columns = array(); + + /** + * @var boolean Mark index for destruction + * @access private + */ + var $drop = FALSE; + + /** + * Initializes the new dbIndex object. + * + * @param object $parent Parent object + * @param array $attributes Attributes + * + * @internal + */ + function __construct( &$parent, $attributes = NULL ) { + $this->parent = $parent; + + $this->name = $this->prefix ($attributes['NAME']); + } + + /** + * XML Callback to process start elements + * + * Processes XML opening tags. + * Elements currently processed are: DROP, CLUSTERED, BITMAP, UNIQUE, FULLTEXT & HASH. + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + $this->currentElement = strtoupper( $tag ); + + switch( $this->currentElement ) { + case 'DROP': + $this->drop(); + break; + case 'CLUSTERED': + case 'BITMAP': + case 'UNIQUE': + case 'FULLTEXT': + case 'HASH': + // Add index Option + $this->addIndexOpt( $this->currentElement ); + break; + default: + // print_r( array( $tag, $attributes ) ); + } + } + + /** + * XML Callback to process CDATA elements + * + * Processes XML cdata. + * + * @access private + */ + function _tag_cdata( &$parser, $cdata ) { + switch( $this->currentElement ) { + // Index field name + case 'COL': + $this->addField( $cdata ); + break; + default: + + } + } + + /** + * XML Callback to process end elements + * + * @access private + */ + function _tag_close( &$parser, $tag ) { + $this->currentElement = ''; + + switch( strtoupper( $tag ) ) { + case 'INDEX': + xml_set_object( $parser, $this->parent ); + break; + } + } + + /** + * Adds a field to the index + * + * @param string $name Field name + * @return string Field list + */ + function addField( $name ) { + $this->columns[$this->FieldID( $name )] = $name; + + // Return the field list + return $this->columns; + } + + /** + * Adds options to the index + * + * @param string $opt Comma-separated list of index options. + * @return string Option list + */ + function addIndexOpt( $opt ) { + $this->opts[] = $opt; + + // Return the options list + return $this->opts; + } + + /** + * Generates the SQL that will create the index in the database + * + * @param object $xmls adoSchema object + * @return array Array containing index creation SQL + */ + function create( &$xmls ) { + if( $this->drop ) { + return NULL; + } + + // eliminate any columns that aren't in the table + foreach( $this->columns as $id => $col ) { + if( !isset( $this->parent->fields[$id] ) ) { + unset( $this->columns[$id] ); + } + } + + return $xmls->dict->CreateIndexSQL( $this->name, $this->parent->name, $this->columns, $this->opts ); + } + + /** + * Marks an index for destruction + */ + function drop() { + $this->drop = TRUE; + } +} + +/** +* Creates a data object in ADOdb's datadict format +* +* This class stores information about table data. +* +* @package axmls +* @access private +*/ +class dbData extends dbObject { + + var $data = array(); + + var $row; + + /** + * Initializes the new dbIndex object. + * + * @param object $parent Parent object + * @param array $attributes Attributes + * + * @internal + */ + function __construct( &$parent, $attributes = NULL ) { + $this->parent = $parent; + } + + /** + * XML Callback to process start elements + * + * Processes XML opening tags. + * Elements currently processed are: DROP, CLUSTERED, BITMAP, UNIQUE, FULLTEXT & HASH. + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + $this->currentElement = strtoupper( $tag ); + + switch( $this->currentElement ) { + case 'ROW': + $this->row = count( $this->data ); + $this->data[$this->row] = array(); + break; + case 'F': + $this->addField($attributes); + default: + // print_r( array( $tag, $attributes ) ); + } + } + + /** + * XML Callback to process CDATA elements + * + * Processes XML cdata. + * + * @access private + */ + function _tag_cdata( &$parser, $cdata ) { + switch( $this->currentElement ) { + // Index field name + case 'F': + $this->addData( $cdata ); + break; + default: + + } + } + + /** + * XML Callback to process end elements + * + * @access private + */ + function _tag_close( &$parser, $tag ) { + $this->currentElement = ''; + + switch( strtoupper( $tag ) ) { + case 'DATA': + xml_set_object( $parser, $this->parent ); + break; + } + } + + /** + * Adds a field to the index + * + * @param string $name Field name + * @return string Field list + */ + function addField( $attributes ) { + if( isset( $attributes['NAME'] ) ) { + $name = $attributes['NAME']; + } else { + $name = count($this->data[$this->row]); + } + + // Set the field index so we know where we are + $this->current_field = $this->FieldID( $name ); + } + + /** + * Adds options to the index + * + * @param string $opt Comma-separated list of index options. + * @return string Option list + */ + function addData( $cdata ) { + if( !isset( $this->data[$this->row] ) ) { + $this->data[$this->row] = array(); + } + + if( !isset( $this->data[$this->row][$this->current_field] ) ) { + $this->data[$this->row][$this->current_field] = ''; + } + + $this->data[$this->row][$this->current_field] .= $cdata; + } + + /** + * Generates the SQL that will create the index in the database + * + * @param object $xmls adoSchema object + * @return array Array containing index creation SQL + */ + function create( &$xmls ) { + $table = $xmls->dict->TableName($this->parent->name); + $table_field_count = count($this->parent->fields); + $sql = array(); + + // eliminate any columns that aren't in the table + foreach( $this->data as $row ) { + $table_fields = $this->parent->fields; + $fields = array(); + + foreach( $row as $field_id => $field_data ) { + if( !array_key_exists( $field_id, $table_fields ) ) { + if( is_numeric( $field_id ) ) { + $field_id = reset( array_keys( $table_fields ) ); + } else { + continue; + } + } + + $name = $table_fields[$field_id]['NAME']; + + switch( $table_fields[$field_id]['TYPE'] ) { + case 'C': + case 'C2': + case 'X': + case 'X2': + $fields[$name] = $xmls->db->qstr( $field_data ); + break; + case 'I': + case 'I1': + case 'I2': + case 'I4': + case 'I8': + $fields[$name] = intval($field_data); + break; + default: + $fields[$name] = $field_data; + } + + unset($table_fields[$field_id]); + } + + // check that at least 1 column is specified + if( empty( $fields ) ) { + continue; + } + + // check that no required columns are missing + if( count( $fields ) < $table_field_count ) { + foreach( $table_fields as $field ) { + if (isset( $field['OPTS'] )) + if( ( in_array( 'NOTNULL', $field['OPTS'] ) || in_array( 'KEY', $field['OPTS'] ) ) && !in_array( 'AUTOINCREMENT', $field['OPTS'] ) ) { + continue(2); + } + } + } + + $sql[] = 'INSERT INTO '. $table .' ('. implode( ',', array_keys( $fields ) ) .') VALUES ('. implode( ',', $fields ) .')'; + } + + return $sql; + } +} + +/** +* Creates the SQL to execute a list of provided SQL queries +* +* @package axmls +* @access private +*/ +class dbQuerySet extends dbObject { + + /** + * @var array List of SQL queries + */ + var $queries = array(); + + /** + * @var string String used to build of a query line by line + */ + var $query; + + /** + * @var string Query prefix key + */ + var $prefixKey = ''; + + /** + * @var boolean Auto prefix enable (TRUE) + */ + var $prefixMethod = 'AUTO'; + + /** + * Initializes the query set. + * + * @param object $parent Parent object + * @param array $attributes Attributes + */ + function __construct( &$parent, $attributes = NULL ) { + $this->parent = $parent; + + // Overrides the manual prefix key + if( isset( $attributes['KEY'] ) ) { + $this->prefixKey = $attributes['KEY']; + } + + $prefixMethod = isset( $attributes['PREFIXMETHOD'] ) ? strtoupper( trim( $attributes['PREFIXMETHOD'] ) ) : ''; + + // Enables or disables automatic prefix prepending + switch( $prefixMethod ) { + case 'AUTO': + $this->prefixMethod = 'AUTO'; + break; + case 'MANUAL': + $this->prefixMethod = 'MANUAL'; + break; + case 'NONE': + $this->prefixMethod = 'NONE'; + break; + } + } + + /** + * XML Callback to process start elements. Elements currently + * processed are: QUERY. + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + $this->currentElement = strtoupper( $tag ); + + switch( $this->currentElement ) { + case 'QUERY': + // Create a new query in a SQL queryset. + // Ignore this query set if a platform is specified and it's different than the + // current connection platform. + if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) { + $this->newQuery(); + } else { + $this->discardQuery(); + } + break; + default: + // print_r( array( $tag, $attributes ) ); + } + } + + /** + * XML Callback to process CDATA elements + */ + function _tag_cdata( &$parser, $cdata ) { + switch( $this->currentElement ) { + // Line of queryset SQL data + case 'QUERY': + $this->buildQuery( $cdata ); + break; + default: + + } + } + + /** + * XML Callback to process end elements + * + * @access private + */ + function _tag_close( &$parser, $tag ) { + $this->currentElement = ''; + + switch( strtoupper( $tag ) ) { + case 'QUERY': + // Add the finished query to the open query set. + $this->addQuery(); + break; + case 'SQL': + $this->parent->addSQL( $this->create( $this->parent ) ); + xml_set_object( $parser, $this->parent ); + $this->destroy(); + break; + default: + + } + } + + /** + * Re-initializes the query. + * + * @return boolean TRUE + */ + function newQuery() { + $this->query = ''; + + return TRUE; + } + + /** + * Discards the existing query. + * + * @return boolean TRUE + */ + function discardQuery() { + unset( $this->query ); + + return TRUE; + } + + /** + * Appends a line to a query that is being built line by line + * + * @param string $data Line of SQL data or NULL to initialize a new query + * @return string SQL query string. + */ + function buildQuery( $sql = NULL ) { + if( !isset( $this->query ) OR empty( $sql ) ) { + return FALSE; + } + + $this->query .= $sql; + + return $this->query; + } + + /** + * Adds a completed query to the query list + * + * @return string SQL of added query + */ + function addQuery() { + if( !isset( $this->query ) ) { + return FALSE; + } + + $this->queries[] = $return = trim($this->query); + + unset( $this->query ); + + return $return; + } + + /** + * Creates and returns the current query set + * + * @param object $xmls adoSchema object + * @return array Query set + */ + function create( &$xmls ) { + foreach( $this->queries as $id => $query ) { + switch( $this->prefixMethod ) { + case 'AUTO': + // Enable auto prefix replacement + + // Process object prefix. + // Evaluate SQL statements to prepend prefix to objects + $query = $this->prefixQuery( '/^\s*((?is)INSERT\s+(INTO\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix ); + $query = $this->prefixQuery( '/^\s*((?is)UPDATE\s+(FROM\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix ); + $query = $this->prefixQuery( '/^\s*((?is)DELETE\s+(FROM\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix ); + + // SELECT statements aren't working yet + #$data = preg_replace( '/(?ias)(^\s*SELECT\s+.*\s+FROM)\s+(\W\s*,?\s*)+((?i)\s+WHERE.*$)/', "\1 $prefix\2 \3", $data ); + + case 'MANUAL': + // If prefixKey is set and has a value then we use it to override the default constant XMLS_PREFIX. + // If prefixKey is not set, we use the default constant XMLS_PREFIX + if( isset( $this->prefixKey ) AND( $this->prefixKey !== '' ) ) { + // Enable prefix override + $query = str_replace( $this->prefixKey, $xmls->objectPrefix, $query ); + } else { + // Use default replacement + $query = str_replace( XMLS_PREFIX , $xmls->objectPrefix, $query ); + } + } + + $this->queries[$id] = trim( $query ); + } + + // Return the query set array + return $this->queries; + } + + /** + * Rebuilds the query with the prefix attached to any objects + * + * @param string $regex Regex used to add prefix + * @param string $query SQL query string + * @param string $prefix Prefix to be appended to tables, indices, etc. + * @return string Prefixed SQL query string. + */ + function prefixQuery( $regex, $query, $prefix = NULL ) { + if( !isset( $prefix ) ) { + return $query; + } + + if( preg_match( $regex, $query, $match ) ) { + $preamble = $match[1]; + $postamble = $match[5]; + $objectList = explode( ',', $match[3] ); + // $prefix = $prefix . '_'; + + $prefixedList = ''; + + foreach( $objectList as $object ) { + if( $prefixedList !== '' ) { + $prefixedList .= ', '; + } + + $prefixedList .= $prefix . trim( $object ); + } + + $query = $preamble . ' ' . $prefixedList . ' ' . $postamble; + } + + return $query; + } +} + +/** +* Loads and parses an XML file, creating an array of "ready-to-run" SQL statements +* +* This class is used to load and parse the XML file, to create an array of SQL statements +* that can be used to build a database, and to build the database using the SQL array. +* +* @tutorial getting_started.pkg +* +* @author Richard Tango-Lowy & Dan Cech +* @version $Revision: 1.12 $ +* +* @package axmls +*/ +class adoSchema { + + /** + * @var array Array containing SQL queries to generate all objects + * @access private + */ + var $sqlArray; + + /** + * @var object ADOdb connection object + * @access private + */ + var $db; + + /** + * @var object ADOdb Data Dictionary + * @access private + */ + var $dict; + + /** + * @var string Current XML element + * @access private + */ + var $currentElement = ''; + + /** + * @var string If set (to 'ALTER' or 'REPLACE'), upgrade an existing database + * @access private + */ + var $upgrade = ''; + + /** + * @var string Optional object prefix + * @access private + */ + var $objectPrefix = ''; + + /** + * @var long Original Magic Quotes Runtime value + * @access private + */ + var $mgq; + + /** + * @var long System debug + * @access private + */ + var $debug; + + /** + * @var string Regular expression to find schema version + * @access private + */ + var $versionRegex = '//'; + + /** + * @var string Current schema version + * @access private + */ + var $schemaVersion; + + /** + * @var int Success of last Schema execution + */ + var $success; + + /** + * @var bool Execute SQL inline as it is generated + */ + var $executeInline; + + /** + * @var bool Continue SQL execution if errors occur + */ + var $continueOnError; + + /** + * Creates an adoSchema object + * + * Creating an adoSchema object is the first step in processing an XML schema. + * The only parameter is an ADOdb database connection object, which must already + * have been created. + * + * @param object $db ADOdb database connection object. + */ + function __construct( $db ) { + // Initialize the environment + $this->mgq = get_magic_quotes_runtime(); + ini_set("magic_quotes_runtime", 0); + #set_magic_quotes_runtime(0); + + $this->db = $db; + $this->debug = $this->db->debug; + $this->dict = NewDataDictionary( $this->db ); + $this->sqlArray = array(); + $this->schemaVersion = XMLS_SCHEMA_VERSION; + $this->executeInline( XMLS_EXECUTE_INLINE ); + $this->continueOnError( XMLS_CONTINUE_ON_ERROR ); + $this->setUpgradeMethod(); + } + + /** + * Sets the method to be used for upgrading an existing database + * + * Use this method to specify how existing database objects should be upgraded. + * The method option can be set to ALTER, REPLACE, BEST, or NONE. ALTER attempts to + * alter each database object directly, REPLACE attempts to rebuild each object + * from scratch, BEST attempts to determine the best upgrade method for each + * object, and NONE disables upgrading. + * + * This method is not yet used by AXMLS, but exists for backward compatibility. + * The ALTER method is automatically assumed when the adoSchema object is + * instantiated; other upgrade methods are not currently supported. + * + * @param string $method Upgrade method (ALTER|REPLACE|BEST|NONE) + * @returns string Upgrade method used + */ + function SetUpgradeMethod( $method = '' ) { + if( !is_string( $method ) ) { + return FALSE; + } + + $method = strtoupper( $method ); + + // Handle the upgrade methods + switch( $method ) { + case 'ALTER': + $this->upgrade = $method; + break; + case 'REPLACE': + $this->upgrade = $method; + break; + case 'BEST': + $this->upgrade = 'ALTER'; + break; + case 'NONE': + $this->upgrade = 'NONE'; + break; + default: + // Use default if no legitimate method is passed. + $this->upgrade = XMLS_DEFAULT_UPGRADE_METHOD; + } + + return $this->upgrade; + } + + /** + * Enables/disables inline SQL execution. + * + * Call this method to enable or disable inline execution of the schema. If the mode is set to TRUE (inline execution), + * AXMLS applies the SQL to the database immediately as each schema entity is parsed. If the mode + * is set to FALSE (post execution), AXMLS parses the entire schema and you will need to call adoSchema::ExecuteSchema() + * to apply the schema to the database. + * + * @param bool $mode execute + * @return bool current execution mode + * + * @see ParseSchema(), ExecuteSchema() + */ + function ExecuteInline( $mode = NULL ) { + if( is_bool( $mode ) ) { + $this->executeInline = $mode; + } + + return $this->executeInline; + } + + /** + * Enables/disables SQL continue on error. + * + * Call this method to enable or disable continuation of SQL execution if an error occurs. + * If the mode is set to TRUE (continue), AXMLS will continue to apply SQL to the database, even if an error occurs. + * If the mode is set to FALSE (halt), AXMLS will halt execution of generated sql if an error occurs, though parsing + * of the schema will continue. + * + * @param bool $mode execute + * @return bool current continueOnError mode + * + * @see addSQL(), ExecuteSchema() + */ + function ContinueOnError( $mode = NULL ) { + if( is_bool( $mode ) ) { + $this->continueOnError = $mode; + } + + return $this->continueOnError; + } + + /** + * Loads an XML schema from a file and converts it to SQL. + * + * Call this method to load the specified schema (see the DTD for the proper format) from + * the filesystem and generate the SQL necessary to create the database described. + * @see ParseSchemaString() + * + * @param string $file Name of XML schema file. + * @param bool $returnSchema Return schema rather than parsing. + * @return array Array of SQL queries, ready to execute + */ + function ParseSchema( $filename, $returnSchema = FALSE ) { + return $this->ParseSchemaString( $this->ConvertSchemaFile( $filename ), $returnSchema ); + } + + /** + * Loads an XML schema from a file and converts it to SQL. + * + * Call this method to load the specified schema from a file (see the DTD for the proper format) + * and generate the SQL necessary to create the database described by the schema. + * + * @param string $file Name of XML schema file. + * @param bool $returnSchema Return schema rather than parsing. + * @return array Array of SQL queries, ready to execute. + * + * @deprecated Replaced by adoSchema::ParseSchema() and adoSchema::ParseSchemaString() + * @see ParseSchema(), ParseSchemaString() + */ + function ParseSchemaFile( $filename, $returnSchema = FALSE ) { + // Open the file + if( !($fp = fopen( $filename, 'r' )) ) { + // die( 'Unable to open file' ); + return FALSE; + } + + // do version detection here + if( $this->SchemaFileVersion( $filename ) != $this->schemaVersion ) { + return FALSE; + } + + if ( $returnSchema ) + { + $xmlstring = ''; + while( $data = fread( $fp, 100000 ) ) { + $xmlstring .= $data; + } + return $xmlstring; + } + + $this->success = 2; + + $xmlParser = $this->create_parser(); + + // Process the file + while( $data = fread( $fp, 4096 ) ) { + if( !xml_parse( $xmlParser, $data, feof( $fp ) ) ) { + die( sprintf( + "XML error: %s at line %d", + xml_error_string( xml_get_error_code( $xmlParser) ), + xml_get_current_line_number( $xmlParser) + ) ); + } + } + + xml_parser_free( $xmlParser ); + + return $this->sqlArray; + } + + /** + * Converts an XML schema string to SQL. + * + * Call this method to parse a string containing an XML schema (see the DTD for the proper format) + * and generate the SQL necessary to create the database described by the schema. + * @see ParseSchema() + * + * @param string $xmlstring XML schema string. + * @param bool $returnSchema Return schema rather than parsing. + * @return array Array of SQL queries, ready to execute. + */ + function ParseSchemaString( $xmlstring, $returnSchema = FALSE ) { + if( !is_string( $xmlstring ) OR empty( $xmlstring ) ) { + return FALSE; + } + + // do version detection here + if( $this->SchemaStringVersion( $xmlstring ) != $this->schemaVersion ) { + return FALSE; + } + + if ( $returnSchema ) + { + return $xmlstring; + } + + $this->success = 2; + + $xmlParser = $this->create_parser(); + + if( !xml_parse( $xmlParser, $xmlstring, TRUE ) ) { + die( sprintf( + "XML error: %s at line %d", + xml_error_string( xml_get_error_code( $xmlParser) ), + xml_get_current_line_number( $xmlParser) + ) ); + } + + xml_parser_free( $xmlParser ); + + return $this->sqlArray; + } + + /** + * Loads an XML schema from a file and converts it to uninstallation SQL. + * + * Call this method to load the specified schema (see the DTD for the proper format) from + * the filesystem and generate the SQL necessary to remove the database described. + * @see RemoveSchemaString() + * + * @param string $file Name of XML schema file. + * @param bool $returnSchema Return schema rather than parsing. + * @return array Array of SQL queries, ready to execute + */ + function RemoveSchema( $filename, $returnSchema = FALSE ) { + return $this->RemoveSchemaString( $this->ConvertSchemaFile( $filename ), $returnSchema ); + } + + /** + * Converts an XML schema string to uninstallation SQL. + * + * Call this method to parse a string containing an XML schema (see the DTD for the proper format) + * and generate the SQL necessary to uninstall the database described by the schema. + * @see RemoveSchema() + * + * @param string $schema XML schema string. + * @param bool $returnSchema Return schema rather than parsing. + * @return array Array of SQL queries, ready to execute. + */ + function RemoveSchemaString( $schema, $returnSchema = FALSE ) { + + // grab current version + if( !( $version = $this->SchemaStringVersion( $schema ) ) ) { + return FALSE; + } + + return $this->ParseSchemaString( $this->TransformSchema( $schema, 'remove-' . $version), $returnSchema ); + } + + /** + * Applies the current XML schema to the database (post execution). + * + * Call this method to apply the current schema (generally created by calling + * ParseSchema() or ParseSchemaString() ) to the database (creating the tables, indexes, + * and executing other SQL specified in the schema) after parsing. + * @see ParseSchema(), ParseSchemaString(), ExecuteInline() + * + * @param array $sqlArray Array of SQL statements that will be applied rather than + * the current schema. + * @param boolean $continueOnErr Continue to apply the schema even if an error occurs. + * @returns integer 0 if failure, 1 if errors, 2 if successful. + */ + function ExecuteSchema( $sqlArray = NULL, $continueOnErr = NULL ) { + if( !is_bool( $continueOnErr ) ) { + $continueOnErr = $this->ContinueOnError(); + } + + if( !isset( $sqlArray ) ) { + $sqlArray = $this->sqlArray; + } + + if( !is_array( $sqlArray ) ) { + $this->success = 0; + } else { + $this->success = $this->dict->ExecuteSQLArray( $sqlArray, $continueOnErr ); + } + + return $this->success; + } + + /** + * Returns the current SQL array. + * + * Call this method to fetch the array of SQL queries resulting from + * ParseSchema() or ParseSchemaString(). + * + * @param string $format Format: HTML, TEXT, or NONE (PHP array) + * @return array Array of SQL statements or FALSE if an error occurs + */ + function PrintSQL( $format = 'NONE' ) { + $sqlArray = null; + return $this->getSQL( $format, $sqlArray ); + } + + /** + * Saves the current SQL array to the local filesystem as a list of SQL queries. + * + * Call this method to save the array of SQL queries (generally resulting from a + * parsed XML schema) to the filesystem. + * + * @param string $filename Path and name where the file should be saved. + * @return boolean TRUE if save is successful, else FALSE. + */ + function SaveSQL( $filename = './schema.sql' ) { + + if( !isset( $sqlArray ) ) { + $sqlArray = $this->sqlArray; + } + if( !isset( $sqlArray ) ) { + return FALSE; + } + + $fp = fopen( $filename, "w" ); + + foreach( $sqlArray as $key => $query ) { + fwrite( $fp, $query . ";\n" ); + } + fclose( $fp ); + } + + /** + * Create an xml parser + * + * @return object PHP XML parser object + * + * @access private + */ + function create_parser() { + // Create the parser + $xmlParser = xml_parser_create(); + xml_set_object( $xmlParser, $this ); + + // Initialize the XML callback functions + xml_set_element_handler( $xmlParser, '_tag_open', '_tag_close' ); + xml_set_character_data_handler( $xmlParser, '_tag_cdata' ); + + return $xmlParser; + } + + /** + * XML Callback to process start elements + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + switch( strtoupper( $tag ) ) { + case 'TABLE': + $this->obj = new dbTable( $this, $attributes ); + xml_set_object( $parser, $this->obj ); + break; + case 'SQL': + if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) { + $this->obj = new dbQuerySet( $this, $attributes ); + xml_set_object( $parser, $this->obj ); + } + break; + default: + // print_r( array( $tag, $attributes ) ); + } + + } + + /** + * XML Callback to process CDATA elements + * + * @access private + */ + function _tag_cdata( &$parser, $cdata ) { + } + + /** + * XML Callback to process end elements + * + * @access private + * @internal + */ + function _tag_close( &$parser, $tag ) { + + } + + /** + * Converts an XML schema string to the specified DTD version. + * + * Call this method to convert a string containing an XML schema to a different AXMLS + * DTD version. For instance, to convert a schema created for an pre-1.0 version for + * AXMLS (DTD version 0.1) to a newer version of the DTD (e.g. 0.2). If no DTD version + * parameter is specified, the schema will be converted to the current DTD version. + * If the newFile parameter is provided, the converted schema will be written to the specified + * file. + * @see ConvertSchemaFile() + * + * @param string $schema String containing XML schema that will be converted. + * @param string $newVersion DTD version to convert to. + * @param string $newFile File name of (converted) output file. + * @return string Converted XML schema or FALSE if an error occurs. + */ + function ConvertSchemaString( $schema, $newVersion = NULL, $newFile = NULL ) { + + // grab current version + if( !( $version = $this->SchemaStringVersion( $schema ) ) ) { + return FALSE; + } + + if( !isset ($newVersion) ) { + $newVersion = $this->schemaVersion; + } + + if( $version == $newVersion ) { + $result = $schema; + } else { + $result = $this->TransformSchema( $schema, 'convert-' . $version . '-' . $newVersion); + } + + if( is_string( $result ) AND is_string( $newFile ) AND ( $fp = fopen( $newFile, 'w' ) ) ) { + fwrite( $fp, $result ); + fclose( $fp ); + } + + return $result; + } + + // compat for pre-4.3 - jlim + function _file_get_contents($path) + { + if (function_exists('file_get_contents')) return file_get_contents($path); + return join('',file($path)); + } + + /** + * Converts an XML schema file to the specified DTD version. + * + * Call this method to convert the specified XML schema file to a different AXMLS + * DTD version. For instance, to convert a schema created for an pre-1.0 version for + * AXMLS (DTD version 0.1) to a newer version of the DTD (e.g. 0.2). If no DTD version + * parameter is specified, the schema will be converted to the current DTD version. + * If the newFile parameter is provided, the converted schema will be written to the specified + * file. + * @see ConvertSchemaString() + * + * @param string $filename Name of XML schema file that will be converted. + * @param string $newVersion DTD version to convert to. + * @param string $newFile File name of (converted) output file. + * @return string Converted XML schema or FALSE if an error occurs. + */ + function ConvertSchemaFile( $filename, $newVersion = NULL, $newFile = NULL ) { + + // grab current version + if( !( $version = $this->SchemaFileVersion( $filename ) ) ) { + return FALSE; + } + + if( !isset ($newVersion) ) { + $newVersion = $this->schemaVersion; + } + + if( $version == $newVersion ) { + $result = _file_get_contents( $filename ); + + // remove unicode BOM if present + if( substr( $result, 0, 3 ) == sprintf( '%c%c%c', 239, 187, 191 ) ) { + $result = substr( $result, 3 ); + } + } else { + $result = $this->TransformSchema( $filename, 'convert-' . $version . '-' . $newVersion, 'file' ); + } + + if( is_string( $result ) AND is_string( $newFile ) AND ( $fp = fopen( $newFile, 'w' ) ) ) { + fwrite( $fp, $result ); + fclose( $fp ); + } + + return $result; + } + + function TransformSchema( $schema, $xsl, $schematype='string' ) + { + // Fail if XSLT extension is not available + if( ! function_exists( 'xslt_create' ) ) { + return FALSE; + } + + $xsl_file = dirname( __FILE__ ) . '/xsl/' . $xsl . '.xsl'; + + // look for xsl + if( !is_readable( $xsl_file ) ) { + return FALSE; + } + + switch( $schematype ) + { + case 'file': + if( !is_readable( $schema ) ) { + return FALSE; + } + + $schema = _file_get_contents( $schema ); + break; + case 'string': + default: + if( !is_string( $schema ) ) { + return FALSE; + } + } + + $arguments = array ( + '/_xml' => $schema, + '/_xsl' => _file_get_contents( $xsl_file ) + ); + + // create an XSLT processor + $xh = xslt_create (); + + // set error handler + xslt_set_error_handler ($xh, array (&$this, 'xslt_error_handler')); + + // process the schema + $result = xslt_process ($xh, 'arg:/_xml', 'arg:/_xsl', NULL, $arguments); + + xslt_free ($xh); + + return $result; + } + + /** + * Processes XSLT transformation errors + * + * @param object $parser XML parser object + * @param integer $errno Error number + * @param integer $level Error level + * @param array $fields Error information fields + * + * @access private + */ + function xslt_error_handler( $parser, $errno, $level, $fields ) { + if( is_array( $fields ) ) { + $msg = array( + 'Message Type' => ucfirst( $fields['msgtype'] ), + 'Message Code' => $fields['code'], + 'Message' => $fields['msg'], + 'Error Number' => $errno, + 'Level' => $level + ); + + switch( $fields['URI'] ) { + case 'arg:/_xml': + $msg['Input'] = 'XML'; + break; + case 'arg:/_xsl': + $msg['Input'] = 'XSL'; + break; + default: + $msg['Input'] = $fields['URI']; + } + + $msg['Line'] = $fields['line']; + } else { + $msg = array( + 'Message Type' => 'Error', + 'Error Number' => $errno, + 'Level' => $level, + 'Fields' => var_export( $fields, TRUE ) + ); + } + + $error_details = $msg['Message Type'] . ' in XSLT Transformation' . "\n" + . '' . "\n"; + + foreach( $msg as $label => $details ) { + $error_details .= '' . "\n"; + } + + $error_details .= '
' . $label . ': ' . htmlentities( $details ) . '
'; + + trigger_error( $error_details, E_USER_ERROR ); + } + + /** + * Returns the AXMLS Schema Version of the requested XML schema file. + * + * Call this method to obtain the AXMLS DTD version of the requested XML schema file. + * @see SchemaStringVersion() + * + * @param string $filename AXMLS schema file + * @return string Schema version number or FALSE on error + */ + function SchemaFileVersion( $filename ) { + // Open the file + if( !($fp = fopen( $filename, 'r' )) ) { + // die( 'Unable to open file' ); + return FALSE; + } + + // Process the file + while( $data = fread( $fp, 4096 ) ) { + if( preg_match( $this->versionRegex, $data, $matches ) ) { + return !empty( $matches[2] ) ? $matches[2] : XMLS_DEFAULT_SCHEMA_VERSION; + } + } + + return FALSE; + } + + /** + * Returns the AXMLS Schema Version of the provided XML schema string. + * + * Call this method to obtain the AXMLS DTD version of the provided XML schema string. + * @see SchemaFileVersion() + * + * @param string $xmlstring XML schema string + * @return string Schema version number or FALSE on error + */ + function SchemaStringVersion( $xmlstring ) { + if( !is_string( $xmlstring ) OR empty( $xmlstring ) ) { + return FALSE; + } + + if( preg_match( $this->versionRegex, $xmlstring, $matches ) ) { + return !empty( $matches[2] ) ? $matches[2] : XMLS_DEFAULT_SCHEMA_VERSION; + } + + return FALSE; + } + + /** + * Extracts an XML schema from an existing database. + * + * Call this method to create an XML schema string from an existing database. + * If the data parameter is set to TRUE, AXMLS will include the data from the database + * in the schema. + * + * @param boolean $data Include data in schema dump + * @return string Generated XML schema + */ + function ExtractSchema( $data = FALSE ) { + $old_mode = $this->db->SetFetchMode( ADODB_FETCH_NUM ); + + $schema = '' . "\n" + . '' . "\n"; + + if( is_array( $tables = $this->db->MetaTables( 'TABLES' ) ) ) { + foreach( $tables as $table ) { + $schema .= ' ' . "\n"; + + // grab details from database + $rs = $this->db->Execute( 'SELECT * FROM ' . $table . ' WHERE 1=1' ); + $fields = $this->db->MetaColumns( $table ); + $indexes = $this->db->MetaIndexes( $table ); + + if( is_array( $fields ) ) { + foreach( $fields as $details ) { + $extra = ''; + $content = array(); + + if( $details->max_length > 0 ) { + $extra .= ' size="' . $details->max_length . '"'; + } + + if( $details->primary_key ) { + $content[] = ''; + } elseif( $details->not_null ) { + $content[] = ''; + } + + if( $details->has_default ) { + $content[] = ''; + } + + if( $details->auto_increment ) { + $content[] = ''; + } + + // this stops the creation of 'R' columns, + // AUTOINCREMENT is used to create auto columns + $details->primary_key = 0; + $type = $rs->MetaType( $details ); + + $schema .= ' '; + + if( !empty( $content ) ) { + $schema .= "\n " . implode( "\n ", $content ) . "\n "; + } + + $schema .= '' . "\n"; + } + } + + if( is_array( $indexes ) ) { + foreach( $indexes as $index => $details ) { + $schema .= ' ' . "\n"; + + if( $details['unique'] ) { + $schema .= ' ' . "\n"; + } + + foreach( $details['columns'] as $column ) { + $schema .= ' ' . $column . '' . "\n"; + } + + $schema .= ' ' . "\n"; + } + } + + if( $data ) { + $rs = $this->db->Execute( 'SELECT * FROM ' . $table ); + + if( is_object( $rs ) ) { + $schema .= ' ' . "\n"; + + while( $row = $rs->FetchRow() ) { + foreach( $row as $key => $val ) { + $row[$key] = htmlentities($val); + } + + $schema .= ' ' . implode( '', $row ) . '' . "\n"; + } + + $schema .= ' ' . "\n"; + } + } + + $schema .= '
' . "\n"; + } + } + + $this->db->SetFetchMode( $old_mode ); + + $schema .= '
'; + return $schema; + } + + /** + * Sets a prefix for database objects + * + * Call this method to set a standard prefix that will be prepended to all database tables + * and indices when the schema is parsed. Calling setPrefix with no arguments clears the prefix. + * + * @param string $prefix Prefix that will be prepended. + * @param boolean $underscore If TRUE, automatically append an underscore character to the prefix. + * @return boolean TRUE if successful, else FALSE + */ + function SetPrefix( $prefix = '', $underscore = TRUE ) { + switch( TRUE ) { + // clear prefix + case empty( $prefix ): + logMsg( 'Cleared prefix' ); + $this->objectPrefix = ''; + return TRUE; + // prefix too long + case strlen( $prefix ) > XMLS_PREFIX_MAXLEN: + // prefix contains invalid characters + case !preg_match( '/^[a-z][a-z0-9_]+$/i', $prefix ): + logMsg( 'Invalid prefix: ' . $prefix ); + return FALSE; + } + + if( $underscore AND substr( $prefix, -1 ) != '_' ) { + $prefix .= '_'; + } + + // prefix valid + logMsg( 'Set prefix: ' . $prefix ); + $this->objectPrefix = $prefix; + return TRUE; + } + + /** + * Returns an object name with the current prefix prepended. + * + * @param string $name Name + * @return string Prefixed name + * + * @access private + */ + function prefix( $name = '' ) { + // if prefix is set + if( !empty( $this->objectPrefix ) ) { + // Prepend the object prefix to the table name + // prepend after quote if used + return preg_replace( '/^(`?)(.+)$/', '$1' . $this->objectPrefix . '$2', $name ); + } + + // No prefix set. Use name provided. + return $name; + } + + /** + * Checks if element references a specific platform + * + * @param string $platform Requested platform + * @returns boolean TRUE if platform check succeeds + * + * @access private + */ + function supportedPlatform( $platform = NULL ) { + $regex = '/^(\w*\|)*' . $this->db->databaseType . '(\|\w*)*$/'; + + if( !isset( $platform ) OR preg_match( $regex, $platform ) ) { + logMsg( "Platform $platform is supported" ); + return TRUE; + } else { + logMsg( "Platform $platform is NOT supported" ); + return FALSE; + } + } + + /** + * Clears the array of generated SQL. + * + * @access private + */ + function clearSQL() { + $this->sqlArray = array(); + } + + /** + * Adds SQL into the SQL array. + * + * @param mixed $sql SQL to Add + * @return boolean TRUE if successful, else FALSE. + * + * @access private + */ + function addSQL( $sql = NULL ) { + if( is_array( $sql ) ) { + foreach( $sql as $line ) { + $this->addSQL( $line ); + } + + return TRUE; + } + + if( is_string( $sql ) ) { + $this->sqlArray[] = $sql; + + // if executeInline is enabled, and either no errors have occurred or continueOnError is enabled, execute SQL. + if( $this->ExecuteInline() && ( $this->success == 2 || $this->ContinueOnError() ) ) { + $saved = $this->db->debug; + $this->db->debug = $this->debug; + $ok = $this->db->Execute( $sql ); + $this->db->debug = $saved; + + if( !$ok ) { + if( $this->debug ) { + ADOConnection::outp( $this->db->ErrorMsg() ); + } + + $this->success = 1; + } + } + + return TRUE; + } + + return FALSE; + } + + /** + * Gets the SQL array in the specified format. + * + * @param string $format Format + * @return mixed SQL + * + * @access private + */ + function getSQL( $format = NULL, $sqlArray = NULL ) { + if( !is_array( $sqlArray ) ) { + $sqlArray = $this->sqlArray; + } + + if( !is_array( $sqlArray ) ) { + return FALSE; + } + + switch( strtolower( $format ) ) { + case 'string': + case 'text': + return !empty( $sqlArray ) ? implode( ";\n\n", $sqlArray ) . ';' : ''; + case'html': + return !empty( $sqlArray ) ? nl2br( htmlentities( implode( ";\n\n", $sqlArray ) . ';' ) ) : ''; + } + + return $this->sqlArray; + } + + /** + * Destroys an adoSchema object. + * + * Call this method to clean up after an adoSchema object that is no longer in use. + * @deprecated adoSchema now cleans up automatically. + */ + function Destroy() { + ini_set("magic_quotes_runtime", $this->mgq ); + #set_magic_quotes_runtime( $this->mgq ); + } +} + +/** +* Message logging function +* +* @access private +*/ +function logMsg( $msg, $title = NULL, $force = FALSE ) { + if( XMLS_DEBUG or $force ) { + echo '
';
+
+		if( isset( $title ) ) {
+			echo '

' . htmlentities( $title ) . '

'; + } + + if( is_object( $this ) ) { + echo '[' . get_class( $this ) . '] '; + } + + print_r( $msg ); + + echo '
'; + } +} diff --git a/app/vendor/adodb/adodb-php/adodb-xmlschema03.inc.php b/app/vendor/adodb/adodb-php/adodb-xmlschema03.inc.php new file mode 100644 index 000000000..c1ecb885d --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb-xmlschema03.inc.php @@ -0,0 +1,2406 @@ +parent = $parent; + } + + /** + * XML Callback to process start elements + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + + } + + /** + * XML Callback to process CDATA elements + * + * @access private + */ + function _tag_cdata( &$parser, $cdata ) { + + } + + /** + * XML Callback to process end elements + * + * @access private + */ + function _tag_close( &$parser, $tag ) { + + } + + function create(&$xmls) { + return array(); + } + + /** + * Destroys the object + */ + function destroy() { + } + + /** + * Checks whether the specified RDBMS is supported by the current + * database object or its ranking ancestor. + * + * @param string $platform RDBMS platform name (from ADODB platform list). + * @return boolean TRUE if RDBMS is supported; otherwise returns FALSE. + */ + function supportedPlatform( $platform = NULL ) { + return is_object( $this->parent ) ? $this->parent->supportedPlatform( $platform ) : TRUE; + } + + /** + * Returns the prefix set by the ranking ancestor of the database object. + * + * @param string $name Prefix string. + * @return string Prefix. + */ + function prefix( $name = '' ) { + return is_object( $this->parent ) ? $this->parent->prefix( $name ) : $name; + } + + /** + * Extracts a field ID from the specified field. + * + * @param string $field Field. + * @return string Field ID. + */ + function FieldID( $field ) { + return strtoupper( preg_replace( '/^`(.+)`$/', '$1', $field ) ); + } +} + +/** +* Creates a table object in ADOdb's datadict format +* +* This class stores information about a database table. As charactaristics +* of the table are loaded from the external source, methods and properties +* of this class are used to build up the table description in ADOdb's +* datadict format. +* +* @package axmls +* @access private +*/ +class dbTable extends dbObject { + + /** + * @var string Table name + */ + var $name; + + /** + * @var array Field specifier: Meta-information about each field + */ + var $fields = array(); + + /** + * @var array List of table indexes. + */ + var $indexes = array(); + + /** + * @var array Table options: Table-level options + */ + var $opts = array(); + + /** + * @var string Field index: Keeps track of which field is currently being processed + */ + var $current_field; + + /** + * @var boolean Mark table for destruction + * @access private + */ + var $drop_table; + + /** + * @var boolean Mark field for destruction (not yet implemented) + * @access private + */ + var $drop_field = array(); + + /** + * @var array Platform-specific options + * @access private + */ + var $currentPlatform = true; + + + /** + * Iniitializes a new table object. + * + * @param string $prefix DB Object prefix + * @param array $attributes Array of table attributes. + */ + function __construct( &$parent, $attributes = NULL ) { + $this->parent = $parent; + $this->name = $this->prefix($attributes['NAME']); + } + + /** + * XML Callback to process start elements. Elements currently + * processed are: INDEX, DROP, FIELD, KEY, NOTNULL, AUTOINCREMENT & DEFAULT. + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + $this->currentElement = strtoupper( $tag ); + + switch( $this->currentElement ) { + case 'INDEX': + if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) { + $index = $this->addIndex( $attributes ); + xml_set_object( $parser, $index ); + } + break; + case 'DATA': + if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) { + $data = $this->addData( $attributes ); + xml_set_object( $parser, $data ); + } + break; + case 'DROP': + $this->drop(); + break; + case 'FIELD': + // Add a field + $fieldName = $attributes['NAME']; + $fieldType = $attributes['TYPE']; + $fieldSize = isset( $attributes['SIZE'] ) ? $attributes['SIZE'] : NULL; + $fieldOpts = !empty( $attributes['OPTS'] ) ? $attributes['OPTS'] : NULL; + + $this->addField( $fieldName, $fieldType, $fieldSize, $fieldOpts ); + break; + case 'KEY': + case 'NOTNULL': + case 'AUTOINCREMENT': + case 'DEFDATE': + case 'DEFTIMESTAMP': + case 'UNSIGNED': + // Add a field option + $this->addFieldOpt( $this->current_field, $this->currentElement ); + break; + case 'DEFAULT': + // Add a field option to the table object + + // Work around ADOdb datadict issue that misinterprets empty strings. + if( $attributes['VALUE'] == '' ) { + $attributes['VALUE'] = " '' "; + } + + $this->addFieldOpt( $this->current_field, $this->currentElement, $attributes['VALUE'] ); + break; + case 'OPT': + case 'CONSTRAINT': + // Accept platform-specific options + $this->currentPlatform = ( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ); + break; + default: + // print_r( array( $tag, $attributes ) ); + } + } + + /** + * XML Callback to process CDATA elements + * + * @access private + */ + function _tag_cdata( &$parser, $cdata ) { + switch( $this->currentElement ) { + // Table/field constraint + case 'CONSTRAINT': + if( isset( $this->current_field ) ) { + $this->addFieldOpt( $this->current_field, $this->currentElement, $cdata ); + } else { + $this->addTableOpt( $cdata ); + } + break; + // Table/field option + case 'OPT': + if( isset( $this->current_field ) ) { + $this->addFieldOpt( $this->current_field, $cdata ); + } else { + $this->addTableOpt( $cdata ); + } + break; + default: + + } + } + + /** + * XML Callback to process end elements + * + * @access private + */ + function _tag_close( &$parser, $tag ) { + $this->currentElement = ''; + + switch( strtoupper( $tag ) ) { + case 'TABLE': + $this->parent->addSQL( $this->create( $this->parent ) ); + xml_set_object( $parser, $this->parent ); + $this->destroy(); + break; + case 'FIELD': + unset($this->current_field); + break; + case 'OPT': + case 'CONSTRAINT': + $this->currentPlatform = true; + break; + default: + + } + } + + /** + * Adds an index to a table object + * + * @param array $attributes Index attributes + * @return object dbIndex object + */ + function addIndex( $attributes ) { + $name = strtoupper( $attributes['NAME'] ); + $this->indexes[$name] = new dbIndex( $this, $attributes ); + return $this->indexes[$name]; + } + + /** + * Adds data to a table object + * + * @param array $attributes Data attributes + * @return object dbData object + */ + function addData( $attributes ) { + if( !isset( $this->data ) ) { + $this->data = new dbData( $this, $attributes ); + } + return $this->data; + } + + /** + * Adds a field to a table object + * + * $name is the name of the table to which the field should be added. + * $type is an ADODB datadict field type. The following field types + * are supported as of ADODB 3.40: + * - C: varchar + * - X: CLOB (character large object) or largest varchar size + * if CLOB is not supported + * - C2: Multibyte varchar + * - X2: Multibyte CLOB + * - B: BLOB (binary large object) + * - D: Date (some databases do not support this, and we return a datetime type) + * - T: Datetime or Timestamp + * - L: Integer field suitable for storing booleans (0 or 1) + * - I: Integer (mapped to I4) + * - I1: 1-byte integer + * - I2: 2-byte integer + * - I4: 4-byte integer + * - I8: 8-byte integer + * - F: Floating point number + * - N: Numeric or decimal number + * + * @param string $name Name of the table to which the field will be added. + * @param string $type ADODB datadict field type. + * @param string $size Field size + * @param array $opts Field options array + * @return array Field specifier array + */ + function addField( $name, $type, $size = NULL, $opts = NULL ) { + $field_id = $this->FieldID( $name ); + + // Set the field index so we know where we are + $this->current_field = $field_id; + + // Set the field name (required) + $this->fields[$field_id]['NAME'] = $name; + + // Set the field type (required) + $this->fields[$field_id]['TYPE'] = $type; + + // Set the field size (optional) + if( isset( $size ) ) { + $this->fields[$field_id]['SIZE'] = $size; + } + + // Set the field options + if( isset( $opts ) ) { + $this->fields[$field_id]['OPTS'] = array($opts); + } else { + $this->fields[$field_id]['OPTS'] = array(); + } + } + + /** + * Adds a field option to the current field specifier + * + * This method adds a field option allowed by the ADOdb datadict + * and appends it to the given field. + * + * @param string $field Field name + * @param string $opt ADOdb field option + * @param mixed $value Field option value + * @return array Field specifier array + */ + function addFieldOpt( $field, $opt, $value = NULL ) { + if( $this->currentPlatform ) { + if( !isset( $value ) ) { + $this->fields[$this->FieldID( $field )]['OPTS'][] = $opt; + // Add the option and value + } else { + $this->fields[$this->FieldID( $field )]['OPTS'][] = array( $opt => $value ); + } + } + } + + /** + * Adds an option to the table + * + * This method takes a comma-separated list of table-level options + * and appends them to the table object. + * + * @param string $opt Table option + * @return array Options + */ + function addTableOpt( $opt ) { + if(isset($this->currentPlatform)) { + $this->opts[$this->parent->db->databaseType] = $opt; + } + return $this->opts; + } + + + /** + * Generates the SQL that will create the table in the database + * + * @param object $xmls adoSchema object + * @return array Array containing table creation SQL + */ + function create( &$xmls ) { + $sql = array(); + + // drop any existing indexes + if( is_array( $legacy_indexes = $xmls->dict->MetaIndexes( $this->name ) ) ) { + foreach( $legacy_indexes as $index => $index_details ) { + $sql[] = $xmls->dict->DropIndexSQL( $index, $this->name ); + } + } + + // remove fields to be dropped from table object + foreach( $this->drop_field as $field ) { + unset( $this->fields[$field] ); + } + + // if table exists + if( is_array( $legacy_fields = $xmls->dict->MetaColumns( $this->name ) ) ) { + // drop table + if( $this->drop_table ) { + $sql[] = $xmls->dict->DropTableSQL( $this->name ); + + return $sql; + } + + // drop any existing fields not in schema + foreach( $legacy_fields as $field_id => $field ) { + if( !isset( $this->fields[$field_id] ) ) { + $sql[] = $xmls->dict->DropColumnSQL( $this->name, $field->name ); + } + } + // if table doesn't exist + } else { + if( $this->drop_table ) { + return $sql; + } + + $legacy_fields = array(); + } + + // Loop through the field specifier array, building the associative array for the field options + $fldarray = array(); + + foreach( $this->fields as $field_id => $finfo ) { + // Set an empty size if it isn't supplied + if( !isset( $finfo['SIZE'] ) ) { + $finfo['SIZE'] = ''; + } + + // Initialize the field array with the type and size + $fldarray[$field_id] = array( + 'NAME' => $finfo['NAME'], + 'TYPE' => $finfo['TYPE'], + 'SIZE' => $finfo['SIZE'] + ); + + // Loop through the options array and add the field options. + if( isset( $finfo['OPTS'] ) ) { + foreach( $finfo['OPTS'] as $opt ) { + // Option has an argument. + if( is_array( $opt ) ) { + $key = key( $opt ); + $value = $opt[key( $opt )]; + @$fldarray[$field_id][$key] .= $value; + // Option doesn't have arguments + } else { + $fldarray[$field_id][$opt] = $opt; + } + } + } + } + + if( empty( $legacy_fields ) ) { + // Create the new table + $sql[] = $xmls->dict->CreateTableSQL( $this->name, $fldarray, $this->opts ); + logMsg( end( $sql ), 'Generated CreateTableSQL' ); + } else { + // Upgrade an existing table + logMsg( "Upgrading {$this->name} using '{$xmls->upgrade}'" ); + switch( $xmls->upgrade ) { + // Use ChangeTableSQL + case 'ALTER': + logMsg( 'Generated ChangeTableSQL (ALTERing table)' ); + $sql[] = $xmls->dict->ChangeTableSQL( $this->name, $fldarray, $this->opts ); + break; + case 'REPLACE': + logMsg( 'Doing upgrade REPLACE (testing)' ); + $sql[] = $xmls->dict->DropTableSQL( $this->name ); + $sql[] = $xmls->dict->CreateTableSQL( $this->name, $fldarray, $this->opts ); + break; + // ignore table + default: + return array(); + } + } + + foreach( $this->indexes as $index ) { + $sql[] = $index->create( $xmls ); + } + + if( isset( $this->data ) ) { + $sql[] = $this->data->create( $xmls ); + } + + return $sql; + } + + /** + * Marks a field or table for destruction + */ + function drop() { + if( isset( $this->current_field ) ) { + // Drop the current field + logMsg( "Dropping field '{$this->current_field}' from table '{$this->name}'" ); + // $this->drop_field[$this->current_field] = $xmls->dict->DropColumnSQL( $this->name, $this->current_field ); + $this->drop_field[$this->current_field] = $this->current_field; + } else { + // Drop the current table + logMsg( "Dropping table '{$this->name}'" ); + // $this->drop_table = $xmls->dict->DropTableSQL( $this->name ); + $this->drop_table = TRUE; + } + } +} + +/** +* Creates an index object in ADOdb's datadict format +* +* This class stores information about a database index. As charactaristics +* of the index are loaded from the external source, methods and properties +* of this class are used to build up the index description in ADOdb's +* datadict format. +* +* @package axmls +* @access private +*/ +class dbIndex extends dbObject { + + /** + * @var string Index name + */ + var $name; + + /** + * @var array Index options: Index-level options + */ + var $opts = array(); + + /** + * @var array Indexed fields: Table columns included in this index + */ + var $columns = array(); + + /** + * @var boolean Mark index for destruction + * @access private + */ + var $drop = FALSE; + + /** + * Initializes the new dbIndex object. + * + * @param object $parent Parent object + * @param array $attributes Attributes + * + * @internal + */ + function __construct( &$parent, $attributes = NULL ) { + $this->parent = $parent; + + $this->name = $this->prefix ($attributes['NAME']); + } + + /** + * XML Callback to process start elements + * + * Processes XML opening tags. + * Elements currently processed are: DROP, CLUSTERED, BITMAP, UNIQUE, FULLTEXT & HASH. + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + $this->currentElement = strtoupper( $tag ); + + switch( $this->currentElement ) { + case 'DROP': + $this->drop(); + break; + case 'CLUSTERED': + case 'BITMAP': + case 'UNIQUE': + case 'FULLTEXT': + case 'HASH': + // Add index Option + $this->addIndexOpt( $this->currentElement ); + break; + default: + // print_r( array( $tag, $attributes ) ); + } + } + + /** + * XML Callback to process CDATA elements + * + * Processes XML cdata. + * + * @access private + */ + function _tag_cdata( &$parser, $cdata ) { + switch( $this->currentElement ) { + // Index field name + case 'COL': + $this->addField( $cdata ); + break; + default: + + } + } + + /** + * XML Callback to process end elements + * + * @access private + */ + function _tag_close( &$parser, $tag ) { + $this->currentElement = ''; + + switch( strtoupper( $tag ) ) { + case 'INDEX': + xml_set_object( $parser, $this->parent ); + break; + } + } + + /** + * Adds a field to the index + * + * @param string $name Field name + * @return string Field list + */ + function addField( $name ) { + $this->columns[$this->FieldID( $name )] = $name; + + // Return the field list + return $this->columns; + } + + /** + * Adds options to the index + * + * @param string $opt Comma-separated list of index options. + * @return string Option list + */ + function addIndexOpt( $opt ) { + $this->opts[] = $opt; + + // Return the options list + return $this->opts; + } + + /** + * Generates the SQL that will create the index in the database + * + * @param object $xmls adoSchema object + * @return array Array containing index creation SQL + */ + function create( &$xmls ) { + if( $this->drop ) { + return NULL; + } + + // eliminate any columns that aren't in the table + foreach( $this->columns as $id => $col ) { + if( !isset( $this->parent->fields[$id] ) ) { + unset( $this->columns[$id] ); + } + } + + return $xmls->dict->CreateIndexSQL( $this->name, $this->parent->name, $this->columns, $this->opts ); + } + + /** + * Marks an index for destruction + */ + function drop() { + $this->drop = TRUE; + } +} + +/** +* Creates a data object in ADOdb's datadict format +* +* This class stores information about table data, and is called +* when we need to load field data into a table. +* +* @package axmls +* @access private +*/ +class dbData extends dbObject { + + var $data = array(); + + var $row; + + /** + * Initializes the new dbData object. + * + * @param object $parent Parent object + * @param array $attributes Attributes + * + * @internal + */ + function __construct( &$parent, $attributes = NULL ) { + $this->parent = $parent; + } + + /** + * XML Callback to process start elements + * + * Processes XML opening tags. + * Elements currently processed are: ROW and F (field). + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + $this->currentElement = strtoupper( $tag ); + + switch( $this->currentElement ) { + case 'ROW': + $this->row = count( $this->data ); + $this->data[$this->row] = array(); + break; + case 'F': + $this->addField($attributes); + default: + // print_r( array( $tag, $attributes ) ); + } + } + + /** + * XML Callback to process CDATA elements + * + * Processes XML cdata. + * + * @access private + */ + function _tag_cdata( &$parser, $cdata ) { + switch( $this->currentElement ) { + // Index field name + case 'F': + $this->addData( $cdata ); + break; + default: + + } + } + + /** + * XML Callback to process end elements + * + * @access private + */ + function _tag_close( &$parser, $tag ) { + $this->currentElement = ''; + + switch( strtoupper( $tag ) ) { + case 'DATA': + xml_set_object( $parser, $this->parent ); + break; + } + } + + /** + * Adds a field to the insert + * + * @param string $name Field name + * @return string Field list + */ + function addField( $attributes ) { + // check we're in a valid row + if( !isset( $this->row ) || !isset( $this->data[$this->row] ) ) { + return; + } + + // Set the field index so we know where we are + if( isset( $attributes['NAME'] ) ) { + $this->current_field = $this->FieldID( $attributes['NAME'] ); + } else { + $this->current_field = count( $this->data[$this->row] ); + } + + // initialise data + if( !isset( $this->data[$this->row][$this->current_field] ) ) { + $this->data[$this->row][$this->current_field] = ''; + } + } + + /** + * Adds options to the index + * + * @param string $opt Comma-separated list of index options. + * @return string Option list + */ + function addData( $cdata ) { + // check we're in a valid field + if ( isset( $this->data[$this->row][$this->current_field] ) ) { + // add data to field + $this->data[$this->row][$this->current_field] .= $cdata; + } + } + + /** + * Generates the SQL that will add/update the data in the database + * + * @param object $xmls adoSchema object + * @return array Array containing index creation SQL + */ + function create( &$xmls ) { + $table = $xmls->dict->TableName($this->parent->name); + $table_field_count = count($this->parent->fields); + $tables = $xmls->db->MetaTables(); + $sql = array(); + + $ukeys = $xmls->db->MetaPrimaryKeys( $table ); + if( !empty( $this->parent->indexes ) and !empty( $ukeys ) ) { + foreach( $this->parent->indexes as $indexObj ) { + if( !in_array( $indexObj->name, $ukeys ) ) $ukeys[] = $indexObj->name; + } + } + + // eliminate any columns that aren't in the table + foreach( $this->data as $row ) { + $table_fields = $this->parent->fields; + $fields = array(); + $rawfields = array(); // Need to keep some of the unprocessed data on hand. + + foreach( $row as $field_id => $field_data ) { + if( !array_key_exists( $field_id, $table_fields ) ) { + if( is_numeric( $field_id ) ) { + $field_id = reset( array_keys( $table_fields ) ); + } else { + continue; + } + } + + $name = $table_fields[$field_id]['NAME']; + + switch( $table_fields[$field_id]['TYPE'] ) { + case 'I': + case 'I1': + case 'I2': + case 'I4': + case 'I8': + $fields[$name] = intval($field_data); + break; + case 'C': + case 'C2': + case 'X': + case 'X2': + default: + $fields[$name] = $xmls->db->qstr( $field_data ); + $rawfields[$name] = $field_data; + } + + unset($table_fields[$field_id]); + + } + + // check that at least 1 column is specified + if( empty( $fields ) ) { + continue; + } + + // check that no required columns are missing + if( count( $fields ) < $table_field_count ) { + foreach( $table_fields as $field ) { + if( isset( $field['OPTS'] ) and ( in_array( 'NOTNULL', $field['OPTS'] ) || in_array( 'KEY', $field['OPTS'] ) ) && !in_array( 'AUTOINCREMENT', $field['OPTS'] ) ) { + continue(2); + } + } + } + + // The rest of this method deals with updating existing data records. + + if( !in_array( $table, $tables ) or ( $mode = $xmls->existingData() ) == XMLS_MODE_INSERT ) { + // Table doesn't yet exist, so it's safe to insert. + logMsg( "$table doesn't exist, inserting or mode is INSERT" ); + $sql[] = 'INSERT INTO '. $table .' ('. implode( ',', array_keys( $fields ) ) .') VALUES ('. implode( ',', $fields ) .')'; + continue; + } + + // Prepare to test for potential violations. Get primary keys and unique indexes + $mfields = array_merge( $fields, $rawfields ); + $keyFields = array_intersect( $ukeys, array_keys( $mfields ) ); + + if( empty( $ukeys ) or count( $keyFields ) == 0 ) { + // No unique keys in schema, so safe to insert + logMsg( "Either schema or data has no unique keys, so safe to insert" ); + $sql[] = 'INSERT INTO '. $table .' ('. implode( ',', array_keys( $fields ) ) .') VALUES ('. implode( ',', $fields ) .')'; + continue; + } + + // Select record containing matching unique keys. + $where = ''; + foreach( $ukeys as $key ) { + if( isset( $mfields[$key] ) and $mfields[$key] ) { + if( $where ) $where .= ' AND '; + $where .= $key . ' = ' . $xmls->db->qstr( $mfields[$key] ); + } + } + $records = $xmls->db->Execute( 'SELECT * FROM ' . $table . ' WHERE ' . $where ); + switch( $records->RecordCount() ) { + case 0: + // No matching record, so safe to insert. + logMsg( "No matching records. Inserting new row with unique data" ); + $sql[] = $xmls->db->GetInsertSQL( $records, $mfields ); + break; + case 1: + // Exactly one matching record, so we can update if the mode permits. + logMsg( "One matching record..." ); + if( $mode == XMLS_MODE_UPDATE ) { + logMsg( "...Updating existing row from unique data" ); + $sql[] = $xmls->db->GetUpdateSQL( $records, $mfields ); + } + break; + default: + // More than one matching record; the result is ambiguous, so we must ignore the row. + logMsg( "More than one matching record. Ignoring row." ); + } + } + return $sql; + } +} + +/** +* Creates the SQL to execute a list of provided SQL queries +* +* @package axmls +* @access private +*/ +class dbQuerySet extends dbObject { + + /** + * @var array List of SQL queries + */ + var $queries = array(); + + /** + * @var string String used to build of a query line by line + */ + var $query; + + /** + * @var string Query prefix key + */ + var $prefixKey = ''; + + /** + * @var boolean Auto prefix enable (TRUE) + */ + var $prefixMethod = 'AUTO'; + + /** + * Initializes the query set. + * + * @param object $parent Parent object + * @param array $attributes Attributes + */ + function __construct( &$parent, $attributes = NULL ) { + $this->parent = $parent; + + // Overrides the manual prefix key + if( isset( $attributes['KEY'] ) ) { + $this->prefixKey = $attributes['KEY']; + } + + $prefixMethod = isset( $attributes['PREFIXMETHOD'] ) ? strtoupper( trim( $attributes['PREFIXMETHOD'] ) ) : ''; + + // Enables or disables automatic prefix prepending + switch( $prefixMethod ) { + case 'AUTO': + $this->prefixMethod = 'AUTO'; + break; + case 'MANUAL': + $this->prefixMethod = 'MANUAL'; + break; + case 'NONE': + $this->prefixMethod = 'NONE'; + break; + } + } + + /** + * XML Callback to process start elements. Elements currently + * processed are: QUERY. + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + $this->currentElement = strtoupper( $tag ); + + switch( $this->currentElement ) { + case 'QUERY': + // Create a new query in a SQL queryset. + // Ignore this query set if a platform is specified and it's different than the + // current connection platform. + if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) { + $this->newQuery(); + } else { + $this->discardQuery(); + } + break; + default: + // print_r( array( $tag, $attributes ) ); + } + } + + /** + * XML Callback to process CDATA elements + */ + function _tag_cdata( &$parser, $cdata ) { + switch( $this->currentElement ) { + // Line of queryset SQL data + case 'QUERY': + $this->buildQuery( $cdata ); + break; + default: + + } + } + + /** + * XML Callback to process end elements + * + * @access private + */ + function _tag_close( &$parser, $tag ) { + $this->currentElement = ''; + + switch( strtoupper( $tag ) ) { + case 'QUERY': + // Add the finished query to the open query set. + $this->addQuery(); + break; + case 'SQL': + $this->parent->addSQL( $this->create( $this->parent ) ); + xml_set_object( $parser, $this->parent ); + $this->destroy(); + break; + default: + + } + } + + /** + * Re-initializes the query. + * + * @return boolean TRUE + */ + function newQuery() { + $this->query = ''; + + return TRUE; + } + + /** + * Discards the existing query. + * + * @return boolean TRUE + */ + function discardQuery() { + unset( $this->query ); + + return TRUE; + } + + /** + * Appends a line to a query that is being built line by line + * + * @param string $data Line of SQL data or NULL to initialize a new query + * @return string SQL query string. + */ + function buildQuery( $sql = NULL ) { + if( !isset( $this->query ) OR empty( $sql ) ) { + return FALSE; + } + + $this->query .= $sql; + + return $this->query; + } + + /** + * Adds a completed query to the query list + * + * @return string SQL of added query + */ + function addQuery() { + if( !isset( $this->query ) ) { + return FALSE; + } + + $this->queries[] = $return = trim($this->query); + + unset( $this->query ); + + return $return; + } + + /** + * Creates and returns the current query set + * + * @param object $xmls adoSchema object + * @return array Query set + */ + function create( &$xmls ) { + foreach( $this->queries as $id => $query ) { + switch( $this->prefixMethod ) { + case 'AUTO': + // Enable auto prefix replacement + + // Process object prefix. + // Evaluate SQL statements to prepend prefix to objects + $query = $this->prefixQuery( '/^\s*((?is)INSERT\s+(INTO\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix ); + $query = $this->prefixQuery( '/^\s*((?is)UPDATE\s+(FROM\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix ); + $query = $this->prefixQuery( '/^\s*((?is)DELETE\s+(FROM\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix ); + + // SELECT statements aren't working yet + #$data = preg_replace( '/(?ias)(^\s*SELECT\s+.*\s+FROM)\s+(\W\s*,?\s*)+((?i)\s+WHERE.*$)/', "\1 $prefix\2 \3", $data ); + + case 'MANUAL': + // If prefixKey is set and has a value then we use it to override the default constant XMLS_PREFIX. + // If prefixKey is not set, we use the default constant XMLS_PREFIX + if( isset( $this->prefixKey ) AND( $this->prefixKey !== '' ) ) { + // Enable prefix override + $query = str_replace( $this->prefixKey, $xmls->objectPrefix, $query ); + } else { + // Use default replacement + $query = str_replace( XMLS_PREFIX , $xmls->objectPrefix, $query ); + } + } + + $this->queries[$id] = trim( $query ); + } + + // Return the query set array + return $this->queries; + } + + /** + * Rebuilds the query with the prefix attached to any objects + * + * @param string $regex Regex used to add prefix + * @param string $query SQL query string + * @param string $prefix Prefix to be appended to tables, indices, etc. + * @return string Prefixed SQL query string. + */ + function prefixQuery( $regex, $query, $prefix = NULL ) { + if( !isset( $prefix ) ) { + return $query; + } + + if( preg_match( $regex, $query, $match ) ) { + $preamble = $match[1]; + $postamble = $match[5]; + $objectList = explode( ',', $match[3] ); + // $prefix = $prefix . '_'; + + $prefixedList = ''; + + foreach( $objectList as $object ) { + if( $prefixedList !== '' ) { + $prefixedList .= ', '; + } + + $prefixedList .= $prefix . trim( $object ); + } + + $query = $preamble . ' ' . $prefixedList . ' ' . $postamble; + } + + return $query; + } +} + +/** +* Loads and parses an XML file, creating an array of "ready-to-run" SQL statements +* +* This class is used to load and parse the XML file, to create an array of SQL statements +* that can be used to build a database, and to build the database using the SQL array. +* +* @tutorial getting_started.pkg +* +* @author Richard Tango-Lowy & Dan Cech +* @version $Revision: 1.62 $ +* +* @package axmls +*/ +class adoSchema { + + /** + * @var array Array containing SQL queries to generate all objects + * @access private + */ + var $sqlArray; + + /** + * @var object ADOdb connection object + * @access private + */ + var $db; + + /** + * @var object ADOdb Data Dictionary + * @access private + */ + var $dict; + + /** + * @var string Current XML element + * @access private + */ + var $currentElement = ''; + + /** + * @var string If set (to 'ALTER' or 'REPLACE'), upgrade an existing database + * @access private + */ + var $upgrade = ''; + + /** + * @var string Optional object prefix + * @access private + */ + var $objectPrefix = ''; + + /** + * @var long Original Magic Quotes Runtime value + * @access private + */ + var $mgq; + + /** + * @var long System debug + * @access private + */ + var $debug; + + /** + * @var string Regular expression to find schema version + * @access private + */ + var $versionRegex = '//'; + + /** + * @var string Current schema version + * @access private + */ + var $schemaVersion; + + /** + * @var int Success of last Schema execution + */ + var $success; + + /** + * @var bool Execute SQL inline as it is generated + */ + var $executeInline; + + /** + * @var bool Continue SQL execution if errors occur + */ + var $continueOnError; + + /** + * @var int How to handle existing data rows (insert, update, or ignore) + */ + var $existingData; + + /** + * Creates an adoSchema object + * + * Creating an adoSchema object is the first step in processing an XML schema. + * The only parameter is an ADOdb database connection object, which must already + * have been created. + * + * @param object $db ADOdb database connection object. + */ + function __construct( $db ) { + // Initialize the environment + $this->mgq = get_magic_quotes_runtime(); + #set_magic_quotes_runtime(0); + ini_set("magic_quotes_runtime", 0); + + $this->db = $db; + $this->debug = $this->db->debug; + $this->dict = NewDataDictionary( $this->db ); + $this->sqlArray = array(); + $this->schemaVersion = XMLS_SCHEMA_VERSION; + $this->executeInline( XMLS_EXECUTE_INLINE ); + $this->continueOnError( XMLS_CONTINUE_ON_ERROR ); + $this->existingData( XMLS_EXISTING_DATA ); + $this->setUpgradeMethod(); + } + + /** + * Sets the method to be used for upgrading an existing database + * + * Use this method to specify how existing database objects should be upgraded. + * The method option can be set to ALTER, REPLACE, BEST, or NONE. ALTER attempts to + * alter each database object directly, REPLACE attempts to rebuild each object + * from scratch, BEST attempts to determine the best upgrade method for each + * object, and NONE disables upgrading. + * + * This method is not yet used by AXMLS, but exists for backward compatibility. + * The ALTER method is automatically assumed when the adoSchema object is + * instantiated; other upgrade methods are not currently supported. + * + * @param string $method Upgrade method (ALTER|REPLACE|BEST|NONE) + * @returns string Upgrade method used + */ + function SetUpgradeMethod( $method = '' ) { + if( !is_string( $method ) ) { + return FALSE; + } + + $method = strtoupper( $method ); + + // Handle the upgrade methods + switch( $method ) { + case 'ALTER': + $this->upgrade = $method; + break; + case 'REPLACE': + $this->upgrade = $method; + break; + case 'BEST': + $this->upgrade = 'ALTER'; + break; + case 'NONE': + $this->upgrade = 'NONE'; + break; + default: + // Use default if no legitimate method is passed. + $this->upgrade = XMLS_DEFAULT_UPGRADE_METHOD; + } + + return $this->upgrade; + } + + /** + * Specifies how to handle existing data row when there is a unique key conflict. + * + * The existingData setting specifies how the parser should handle existing rows + * when a unique key violation occurs during the insert. This can happen when inserting + * data into an existing table with one or more primary keys or unique indexes. + * The existingData method takes one of three options: XMLS_MODE_INSERT attempts + * to always insert the data as a new row. In the event of a unique key violation, + * the database will generate an error. XMLS_MODE_UPDATE attempts to update the + * any existing rows with the new data based upon primary or unique key fields in + * the schema. If the data row in the schema specifies no unique fields, the row + * data will be inserted as a new row. XMLS_MODE_IGNORE specifies that any data rows + * that would result in a unique key violation be ignored; no inserts or updates will + * take place. For backward compatibility, the default setting is XMLS_MODE_INSERT, + * but XMLS_MODE_UPDATE will generally be the most appropriate setting. + * + * @param int $mode XMLS_MODE_INSERT, XMLS_MODE_UPDATE, or XMLS_MODE_IGNORE + * @return int current mode + */ + function ExistingData( $mode = NULL ) { + if( is_int( $mode ) ) { + switch( $mode ) { + case XMLS_MODE_UPDATE: + $mode = XMLS_MODE_UPDATE; + break; + case XMLS_MODE_IGNORE: + $mode = XMLS_MODE_IGNORE; + break; + case XMLS_MODE_INSERT: + $mode = XMLS_MODE_INSERT; + break; + default: + $mode = XMLS_EXISTING_DATA; + break; + } + $this->existingData = $mode; + } + + return $this->existingData; + } + + /** + * Enables/disables inline SQL execution. + * + * Call this method to enable or disable inline execution of the schema. If the mode is set to TRUE (inline execution), + * AXMLS applies the SQL to the database immediately as each schema entity is parsed. If the mode + * is set to FALSE (post execution), AXMLS parses the entire schema and you will need to call adoSchema::ExecuteSchema() + * to apply the schema to the database. + * + * @param bool $mode execute + * @return bool current execution mode + * + * @see ParseSchema(), ExecuteSchema() + */ + function ExecuteInline( $mode = NULL ) { + if( is_bool( $mode ) ) { + $this->executeInline = $mode; + } + + return $this->executeInline; + } + + /** + * Enables/disables SQL continue on error. + * + * Call this method to enable or disable continuation of SQL execution if an error occurs. + * If the mode is set to TRUE (continue), AXMLS will continue to apply SQL to the database, even if an error occurs. + * If the mode is set to FALSE (halt), AXMLS will halt execution of generated sql if an error occurs, though parsing + * of the schema will continue. + * + * @param bool $mode execute + * @return bool current continueOnError mode + * + * @see addSQL(), ExecuteSchema() + */ + function ContinueOnError( $mode = NULL ) { + if( is_bool( $mode ) ) { + $this->continueOnError = $mode; + } + + return $this->continueOnError; + } + + /** + * Loads an XML schema from a file and converts it to SQL. + * + * Call this method to load the specified schema (see the DTD for the proper format) from + * the filesystem and generate the SQL necessary to create the database + * described. This method automatically converts the schema to the latest + * axmls schema version. + * @see ParseSchemaString() + * + * @param string $file Name of XML schema file. + * @param bool $returnSchema Return schema rather than parsing. + * @return array Array of SQL queries, ready to execute + */ + function ParseSchema( $filename, $returnSchema = FALSE ) { + return $this->ParseSchemaString( $this->ConvertSchemaFile( $filename ), $returnSchema ); + } + + /** + * Loads an XML schema from a file and converts it to SQL. + * + * Call this method to load the specified schema directly from a file (see + * the DTD for the proper format) and generate the SQL necessary to create + * the database described by the schema. Use this method when you are dealing + * with large schema files. Otherwise, ParseSchema() is faster. + * This method does not automatically convert the schema to the latest axmls + * schema version. You must convert the schema manually using either the + * ConvertSchemaFile() or ConvertSchemaString() method. + * @see ParseSchema() + * @see ConvertSchemaFile() + * @see ConvertSchemaString() + * + * @param string $file Name of XML schema file. + * @param bool $returnSchema Return schema rather than parsing. + * @return array Array of SQL queries, ready to execute. + * + * @deprecated Replaced by adoSchema::ParseSchema() and adoSchema::ParseSchemaString() + * @see ParseSchema(), ParseSchemaString() + */ + function ParseSchemaFile( $filename, $returnSchema = FALSE ) { + // Open the file + if( !($fp = fopen( $filename, 'r' )) ) { + logMsg( 'Unable to open file' ); + return FALSE; + } + + // do version detection here + if( $this->SchemaFileVersion( $filename ) != $this->schemaVersion ) { + logMsg( 'Invalid Schema Version' ); + return FALSE; + } + + if( $returnSchema ) { + $xmlstring = ''; + while( $data = fread( $fp, 4096 ) ) { + $xmlstring .= $data . "\n"; + } + return $xmlstring; + } + + $this->success = 2; + + $xmlParser = $this->create_parser(); + + // Process the file + while( $data = fread( $fp, 4096 ) ) { + if( !xml_parse( $xmlParser, $data, feof( $fp ) ) ) { + die( sprintf( + "XML error: %s at line %d", + xml_error_string( xml_get_error_code( $xmlParser) ), + xml_get_current_line_number( $xmlParser) + ) ); + } + } + + xml_parser_free( $xmlParser ); + + return $this->sqlArray; + } + + /** + * Converts an XML schema string to SQL. + * + * Call this method to parse a string containing an XML schema (see the DTD for the proper format) + * and generate the SQL necessary to create the database described by the schema. + * @see ParseSchema() + * + * @param string $xmlstring XML schema string. + * @param bool $returnSchema Return schema rather than parsing. + * @return array Array of SQL queries, ready to execute. + */ + function ParseSchemaString( $xmlstring, $returnSchema = FALSE ) { + if( !is_string( $xmlstring ) OR empty( $xmlstring ) ) { + logMsg( 'Empty or Invalid Schema' ); + return FALSE; + } + + // do version detection here + if( $this->SchemaStringVersion( $xmlstring ) != $this->schemaVersion ) { + logMsg( 'Invalid Schema Version' ); + return FALSE; + } + + if( $returnSchema ) { + return $xmlstring; + } + + $this->success = 2; + + $xmlParser = $this->create_parser(); + + if( !xml_parse( $xmlParser, $xmlstring, TRUE ) ) { + die( sprintf( + "XML error: %s at line %d", + xml_error_string( xml_get_error_code( $xmlParser) ), + xml_get_current_line_number( $xmlParser) + ) ); + } + + xml_parser_free( $xmlParser ); + + return $this->sqlArray; + } + + /** + * Loads an XML schema from a file and converts it to uninstallation SQL. + * + * Call this method to load the specified schema (see the DTD for the proper format) from + * the filesystem and generate the SQL necessary to remove the database described. + * @see RemoveSchemaString() + * + * @param string $file Name of XML schema file. + * @param bool $returnSchema Return schema rather than parsing. + * @return array Array of SQL queries, ready to execute + */ + function RemoveSchema( $filename, $returnSchema = FALSE ) { + return $this->RemoveSchemaString( $this->ConvertSchemaFile( $filename ), $returnSchema ); + } + + /** + * Converts an XML schema string to uninstallation SQL. + * + * Call this method to parse a string containing an XML schema (see the DTD for the proper format) + * and generate the SQL necessary to uninstall the database described by the schema. + * @see RemoveSchema() + * + * @param string $schema XML schema string. + * @param bool $returnSchema Return schema rather than parsing. + * @return array Array of SQL queries, ready to execute. + */ + function RemoveSchemaString( $schema, $returnSchema = FALSE ) { + + // grab current version + if( !( $version = $this->SchemaStringVersion( $schema ) ) ) { + return FALSE; + } + + return $this->ParseSchemaString( $this->TransformSchema( $schema, 'remove-' . $version), $returnSchema ); + } + + /** + * Applies the current XML schema to the database (post execution). + * + * Call this method to apply the current schema (generally created by calling + * ParseSchema() or ParseSchemaString() ) to the database (creating the tables, indexes, + * and executing other SQL specified in the schema) after parsing. + * @see ParseSchema(), ParseSchemaString(), ExecuteInline() + * + * @param array $sqlArray Array of SQL statements that will be applied rather than + * the current schema. + * @param boolean $continueOnErr Continue to apply the schema even if an error occurs. + * @returns integer 0 if failure, 1 if errors, 2 if successful. + */ + function ExecuteSchema( $sqlArray = NULL, $continueOnErr = NULL ) { + if( !is_bool( $continueOnErr ) ) { + $continueOnErr = $this->ContinueOnError(); + } + + if( !isset( $sqlArray ) ) { + $sqlArray = $this->sqlArray; + } + + if( !is_array( $sqlArray ) ) { + $this->success = 0; + } else { + $this->success = $this->dict->ExecuteSQLArray( $sqlArray, $continueOnErr ); + } + + return $this->success; + } + + /** + * Returns the current SQL array. + * + * Call this method to fetch the array of SQL queries resulting from + * ParseSchema() or ParseSchemaString(). + * + * @param string $format Format: HTML, TEXT, or NONE (PHP array) + * @return array Array of SQL statements or FALSE if an error occurs + */ + function PrintSQL( $format = 'NONE' ) { + $sqlArray = null; + return $this->getSQL( $format, $sqlArray ); + } + + /** + * Saves the current SQL array to the local filesystem as a list of SQL queries. + * + * Call this method to save the array of SQL queries (generally resulting from a + * parsed XML schema) to the filesystem. + * + * @param string $filename Path and name where the file should be saved. + * @return boolean TRUE if save is successful, else FALSE. + */ + function SaveSQL( $filename = './schema.sql' ) { + + if( !isset( $sqlArray ) ) { + $sqlArray = $this->sqlArray; + } + if( !isset( $sqlArray ) ) { + return FALSE; + } + + $fp = fopen( $filename, "w" ); + + foreach( $sqlArray as $key => $query ) { + fwrite( $fp, $query . ";\n" ); + } + fclose( $fp ); + } + + /** + * Create an xml parser + * + * @return object PHP XML parser object + * + * @access private + */ + function create_parser() { + // Create the parser + $xmlParser = xml_parser_create(); + xml_set_object( $xmlParser, $this ); + + // Initialize the XML callback functions + xml_set_element_handler( $xmlParser, '_tag_open', '_tag_close' ); + xml_set_character_data_handler( $xmlParser, '_tag_cdata' ); + + return $xmlParser; + } + + /** + * XML Callback to process start elements + * + * @access private + */ + function _tag_open( &$parser, $tag, $attributes ) { + switch( strtoupper( $tag ) ) { + case 'TABLE': + if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) { + $this->obj = new dbTable( $this, $attributes ); + xml_set_object( $parser, $this->obj ); + } + break; + case 'SQL': + if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) { + $this->obj = new dbQuerySet( $this, $attributes ); + xml_set_object( $parser, $this->obj ); + } + break; + default: + // print_r( array( $tag, $attributes ) ); + } + + } + + /** + * XML Callback to process CDATA elements + * + * @access private + */ + function _tag_cdata( &$parser, $cdata ) { + } + + /** + * XML Callback to process end elements + * + * @access private + * @internal + */ + function _tag_close( &$parser, $tag ) { + + } + + /** + * Converts an XML schema string to the specified DTD version. + * + * Call this method to convert a string containing an XML schema to a different AXMLS + * DTD version. For instance, to convert a schema created for an pre-1.0 version for + * AXMLS (DTD version 0.1) to a newer version of the DTD (e.g. 0.2). If no DTD version + * parameter is specified, the schema will be converted to the current DTD version. + * If the newFile parameter is provided, the converted schema will be written to the specified + * file. + * @see ConvertSchemaFile() + * + * @param string $schema String containing XML schema that will be converted. + * @param string $newVersion DTD version to convert to. + * @param string $newFile File name of (converted) output file. + * @return string Converted XML schema or FALSE if an error occurs. + */ + function ConvertSchemaString( $schema, $newVersion = NULL, $newFile = NULL ) { + + // grab current version + if( !( $version = $this->SchemaStringVersion( $schema ) ) ) { + return FALSE; + } + + if( !isset ($newVersion) ) { + $newVersion = $this->schemaVersion; + } + + if( $version == $newVersion ) { + $result = $schema; + } else { + $result = $this->TransformSchema( $schema, 'convert-' . $version . '-' . $newVersion); + } + + if( is_string( $result ) AND is_string( $newFile ) AND ( $fp = fopen( $newFile, 'w' ) ) ) { + fwrite( $fp, $result ); + fclose( $fp ); + } + + return $result; + } + + /* + // compat for pre-4.3 - jlim + function _file_get_contents($path) + { + if (function_exists('file_get_contents')) return file_get_contents($path); + return join('',file($path)); + }*/ + + /** + * Converts an XML schema file to the specified DTD version. + * + * Call this method to convert the specified XML schema file to a different AXMLS + * DTD version. For instance, to convert a schema created for an pre-1.0 version for + * AXMLS (DTD version 0.1) to a newer version of the DTD (e.g. 0.2). If no DTD version + * parameter is specified, the schema will be converted to the current DTD version. + * If the newFile parameter is provided, the converted schema will be written to the specified + * file. + * @see ConvertSchemaString() + * + * @param string $filename Name of XML schema file that will be converted. + * @param string $newVersion DTD version to convert to. + * @param string $newFile File name of (converted) output file. + * @return string Converted XML schema or FALSE if an error occurs. + */ + function ConvertSchemaFile( $filename, $newVersion = NULL, $newFile = NULL ) { + + // grab current version + if( !( $version = $this->SchemaFileVersion( $filename ) ) ) { + return FALSE; + } + + if( !isset ($newVersion) ) { + $newVersion = $this->schemaVersion; + } + + if( $version == $newVersion ) { + $result = _file_get_contents( $filename ); + + // remove unicode BOM if present + if( substr( $result, 0, 3 ) == sprintf( '%c%c%c', 239, 187, 191 ) ) { + $result = substr( $result, 3 ); + } + } else { + $result = $this->TransformSchema( $filename, 'convert-' . $version . '-' . $newVersion, 'file' ); + } + + if( is_string( $result ) AND is_string( $newFile ) AND ( $fp = fopen( $newFile, 'w' ) ) ) { + fwrite( $fp, $result ); + fclose( $fp ); + } + + return $result; + } + + function TransformSchema( $schema, $xsl, $schematype='string' ) + { + // Fail if XSLT extension is not available + if( ! function_exists( 'xslt_create' ) ) { + return FALSE; + } + + $xsl_file = dirname( __FILE__ ) . '/xsl/' . $xsl . '.xsl'; + + // look for xsl + if( !is_readable( $xsl_file ) ) { + return FALSE; + } + + switch( $schematype ) + { + case 'file': + if( !is_readable( $schema ) ) { + return FALSE; + } + + $schema = _file_get_contents( $schema ); + break; + case 'string': + default: + if( !is_string( $schema ) ) { + return FALSE; + } + } + + $arguments = array ( + '/_xml' => $schema, + '/_xsl' => _file_get_contents( $xsl_file ) + ); + + // create an XSLT processor + $xh = xslt_create (); + + // set error handler + xslt_set_error_handler ($xh, array (&$this, 'xslt_error_handler')); + + // process the schema + $result = xslt_process ($xh, 'arg:/_xml', 'arg:/_xsl', NULL, $arguments); + + xslt_free ($xh); + + return $result; + } + + /** + * Processes XSLT transformation errors + * + * @param object $parser XML parser object + * @param integer $errno Error number + * @param integer $level Error level + * @param array $fields Error information fields + * + * @access private + */ + function xslt_error_handler( $parser, $errno, $level, $fields ) { + if( is_array( $fields ) ) { + $msg = array( + 'Message Type' => ucfirst( $fields['msgtype'] ), + 'Message Code' => $fields['code'], + 'Message' => $fields['msg'], + 'Error Number' => $errno, + 'Level' => $level + ); + + switch( $fields['URI'] ) { + case 'arg:/_xml': + $msg['Input'] = 'XML'; + break; + case 'arg:/_xsl': + $msg['Input'] = 'XSL'; + break; + default: + $msg['Input'] = $fields['URI']; + } + + $msg['Line'] = $fields['line']; + } else { + $msg = array( + 'Message Type' => 'Error', + 'Error Number' => $errno, + 'Level' => $level, + 'Fields' => var_export( $fields, TRUE ) + ); + } + + $error_details = $msg['Message Type'] . ' in XSLT Transformation' . "\n" + . '' . "\n"; + + foreach( $msg as $label => $details ) { + $error_details .= '' . "\n"; + } + + $error_details .= '
' . $label . ': ' . htmlentities( $details ) . '
'; + + trigger_error( $error_details, E_USER_ERROR ); + } + + /** + * Returns the AXMLS Schema Version of the requested XML schema file. + * + * Call this method to obtain the AXMLS DTD version of the requested XML schema file. + * @see SchemaStringVersion() + * + * @param string $filename AXMLS schema file + * @return string Schema version number or FALSE on error + */ + function SchemaFileVersion( $filename ) { + // Open the file + if( !($fp = fopen( $filename, 'r' )) ) { + // die( 'Unable to open file' ); + return FALSE; + } + + // Process the file + while( $data = fread( $fp, 4096 ) ) { + if( preg_match( $this->versionRegex, $data, $matches ) ) { + return !empty( $matches[2] ) ? $matches[2] : XMLS_DEFAULT_SCHEMA_VERSION; + } + } + + return FALSE; + } + + /** + * Returns the AXMLS Schema Version of the provided XML schema string. + * + * Call this method to obtain the AXMLS DTD version of the provided XML schema string. + * @see SchemaFileVersion() + * + * @param string $xmlstring XML schema string + * @return string Schema version number or FALSE on error + */ + function SchemaStringVersion( $xmlstring ) { + if( !is_string( $xmlstring ) OR empty( $xmlstring ) ) { + return FALSE; + } + + if( preg_match( $this->versionRegex, $xmlstring, $matches ) ) { + return !empty( $matches[2] ) ? $matches[2] : XMLS_DEFAULT_SCHEMA_VERSION; + } + + return FALSE; + } + + /** + * Extracts an XML schema from an existing database. + * + * Call this method to create an XML schema string from an existing database. + * If the data parameter is set to TRUE, AXMLS will include the data from the database + * in the schema. + * + * @param boolean $data Include data in schema dump + * @indent string indentation to use + * @prefix string extract only tables with given prefix + * @stripprefix strip prefix string when storing in XML schema + * @return string Generated XML schema + */ + function ExtractSchema( $data = FALSE, $indent = ' ', $prefix = '' , $stripprefix=false) { + $old_mode = $this->db->SetFetchMode( ADODB_FETCH_NUM ); + + $schema = '' . "\n" + . '' . "\n"; + if( is_array( $tables = $this->db->MetaTables( 'TABLES' ,false ,($prefix) ? str_replace('_','\_',$prefix).'%' : '') ) ) { + foreach( $tables as $table ) { + $schema .= $indent + . '' . "\n"; + + // grab details from database + $rs = $this->db->Execute( 'SELECT * FROM ' . $table . ' WHERE -1' ); + $fields = $this->db->MetaColumns( $table ); + $indexes = $this->db->MetaIndexes( $table ); + + if( is_array( $fields ) ) { + foreach( $fields as $details ) { + $extra = ''; + $content = array(); + + if( isset($details->max_length) && $details->max_length > 0 ) { + $extra .= ' size="' . $details->max_length . '"'; + } + + if( isset($details->primary_key) && $details->primary_key ) { + $content[] = ''; + } elseif( isset($details->not_null) && $details->not_null ) { + $content[] = ''; + } + + if( isset($details->has_default) && $details->has_default ) { + $content[] = ''; + } + + if( isset($details->auto_increment) && $details->auto_increment ) { + $content[] = ''; + } + + if( isset($details->unsigned) && $details->unsigned ) { + $content[] = ''; + } + + // this stops the creation of 'R' columns, + // AUTOINCREMENT is used to create auto columns + $details->primary_key = 0; + $type = $rs->MetaType( $details ); + + $schema .= str_repeat( $indent, 2 ) . '' . "\n"; + } else { + $schema .= "/>\n"; + } + } + } + + if( is_array( $indexes ) ) { + foreach( $indexes as $index => $details ) { + $schema .= str_repeat( $indent, 2 ) . '' . "\n"; + + if( $details['unique'] ) { + $schema .= str_repeat( $indent, 3 ) . '' . "\n"; + } + + foreach( $details['columns'] as $column ) { + $schema .= str_repeat( $indent, 3 ) . '' . htmlentities( $column ) . '' . "\n"; + } + + $schema .= str_repeat( $indent, 2 ) . '' . "\n"; + } + } + + if( $data ) { + $rs = $this->db->Execute( 'SELECT * FROM ' . $table ); + + if( is_object( $rs ) && !$rs->EOF ) { + $schema .= str_repeat( $indent, 2 ) . "\n"; + + while( $row = $rs->FetchRow() ) { + foreach( $row as $key => $val ) { + if ( $val != htmlentities( $val ) ) { + $row[$key] = ''; + } + } + + $schema .= str_repeat( $indent, 3 ) . '' . implode( '', $row ) . "\n"; + } + + $schema .= str_repeat( $indent, 2 ) . "\n"; + } + } + + $schema .= $indent . "
\n"; + } + } + + $this->db->SetFetchMode( $old_mode ); + + $schema .= '
'; + return $schema; + } + + /** + * Sets a prefix for database objects + * + * Call this method to set a standard prefix that will be prepended to all database tables + * and indices when the schema is parsed. Calling setPrefix with no arguments clears the prefix. + * + * @param string $prefix Prefix that will be prepended. + * @param boolean $underscore If TRUE, automatically append an underscore character to the prefix. + * @return boolean TRUE if successful, else FALSE + */ + function SetPrefix( $prefix = '', $underscore = TRUE ) { + switch( TRUE ) { + // clear prefix + case empty( $prefix ): + logMsg( 'Cleared prefix' ); + $this->objectPrefix = ''; + return TRUE; + // prefix too long + case strlen( $prefix ) > XMLS_PREFIX_MAXLEN: + // prefix contains invalid characters + case !preg_match( '/^[a-z][a-z0-9_]+$/i', $prefix ): + logMsg( 'Invalid prefix: ' . $prefix ); + return FALSE; + } + + if( $underscore AND substr( $prefix, -1 ) != '_' ) { + $prefix .= '_'; + } + + // prefix valid + logMsg( 'Set prefix: ' . $prefix ); + $this->objectPrefix = $prefix; + return TRUE; + } + + /** + * Returns an object name with the current prefix prepended. + * + * @param string $name Name + * @return string Prefixed name + * + * @access private + */ + function prefix( $name = '' ) { + // if prefix is set + if( !empty( $this->objectPrefix ) ) { + // Prepend the object prefix to the table name + // prepend after quote if used + return preg_replace( '/^(`?)(.+)$/', '$1' . $this->objectPrefix . '$2', $name ); + } + + // No prefix set. Use name provided. + return $name; + } + + /** + * Checks if element references a specific platform + * + * @param string $platform Requested platform + * @returns boolean TRUE if platform check succeeds + * + * @access private + */ + function supportedPlatform( $platform = NULL ) { + if( !empty( $platform ) ) { + $regex = '/(^|\|)' . $this->db->databaseType . '(\||$)/i'; + + if( preg_match( '/^- /', $platform ) ) { + if (preg_match ( $regex, substr( $platform, 2 ) ) ) { + logMsg( 'Platform ' . $platform . ' is NOT supported' ); + return FALSE; + } + } else { + if( !preg_match ( $regex, $platform ) ) { + logMsg( 'Platform ' . $platform . ' is NOT supported' ); + return FALSE; + } + } + } + + logMsg( 'Platform ' . $platform . ' is supported' ); + return TRUE; + } + + /** + * Clears the array of generated SQL. + * + * @access private + */ + function clearSQL() { + $this->sqlArray = array(); + } + + /** + * Adds SQL into the SQL array. + * + * @param mixed $sql SQL to Add + * @return boolean TRUE if successful, else FALSE. + * + * @access private + */ + function addSQL( $sql = NULL ) { + if( is_array( $sql ) ) { + foreach( $sql as $line ) { + $this->addSQL( $line ); + } + + return TRUE; + } + + if( is_string( $sql ) ) { + $this->sqlArray[] = $sql; + + // if executeInline is enabled, and either no errors have occurred or continueOnError is enabled, execute SQL. + if( $this->ExecuteInline() && ( $this->success == 2 || $this->ContinueOnError() ) ) { + $saved = $this->db->debug; + $this->db->debug = $this->debug; + $ok = $this->db->Execute( $sql ); + $this->db->debug = $saved; + + if( !$ok ) { + if( $this->debug ) { + ADOConnection::outp( $this->db->ErrorMsg() ); + } + + $this->success = 1; + } + } + + return TRUE; + } + + return FALSE; + } + + /** + * Gets the SQL array in the specified format. + * + * @param string $format Format + * @return mixed SQL + * + * @access private + */ + function getSQL( $format = NULL, $sqlArray = NULL ) { + if( !is_array( $sqlArray ) ) { + $sqlArray = $this->sqlArray; + } + + if( !is_array( $sqlArray ) ) { + return FALSE; + } + + switch( strtolower( $format ) ) { + case 'string': + case 'text': + return !empty( $sqlArray ) ? implode( ";\n\n", $sqlArray ) . ';' : ''; + case'html': + return !empty( $sqlArray ) ? nl2br( htmlentities( implode( ";\n\n", $sqlArray ) . ';' ) ) : ''; + } + + return $this->sqlArray; + } + + /** + * Destroys an adoSchema object. + * + * Call this method to clean up after an adoSchema object that is no longer in use. + * @deprecated adoSchema now cleans up automatically. + */ + function Destroy() { + ini_set("magic_quotes_runtime", $this->mgq ); + #set_magic_quotes_runtime( $this->mgq ); + } +} + +/** +* Message logging function +* +* @access private +*/ +function logMsg( $msg, $title = NULL, $force = FALSE ) { + if( XMLS_DEBUG or $force ) { + echo '
';
+
+		if( isset( $title ) ) {
+			echo '

' . htmlentities( $title ) . '

'; + } + + if( @is_object( $this ) ) { + echo '[' . get_class( $this ) . '] '; + } + + print_r( $msg ); + + echo '
'; + } +} diff --git a/app/vendor/adodb/adodb-php/adodb.inc.php b/app/vendor/adodb/adodb-php/adodb.inc.php new file mode 100644 index 000000000..62607a258 --- /dev/null +++ b/app/vendor/adodb/adodb-php/adodb.inc.php @@ -0,0 +1,4975 @@ +fields is available on EOF + $ADODB_FETCH_MODE, // DEFAULT, NUM, ASSOC or BOTH. Default follows native driver default... + $ADODB_GETONE_EOF, + $ADODB_QUOTE_FIELDNAMES; // Allows you to force quotes (backticks) around field names in queries generated by getinsertsql and getupdatesql. + + //============================================================================================== + // GLOBAL SETUP + //============================================================================================== + + $ADODB_EXTENSION = defined('ADODB_EXTENSION'); + + // ******************************************************** + // Controls $ADODB_FORCE_TYPE mode. Default is ADODB_FORCE_VALUE (3). + // Used in GetUpdateSql and GetInsertSql functions. Thx to Niko, nuko#mbnet.fi + // + // 0 = ignore empty fields. All empty fields in array are ignored. + // 1 = force null. All empty, php null and string 'null' fields are changed to sql NULL values. + // 2 = force empty. All empty, php null and string 'null' fields are changed to sql empty '' or 0 values. + // 3 = force value. Value is left as it is. Php null and string 'null' are set to sql NULL values and empty fields '' are set to empty '' sql values. + + define('ADODB_FORCE_IGNORE',0); + define('ADODB_FORCE_NULL',1); + define('ADODB_FORCE_EMPTY',2); + define('ADODB_FORCE_VALUE',3); + // ******************************************************** + + + if (!$ADODB_EXTENSION || ADODB_EXTENSION < 4.0) { + + define('ADODB_BAD_RS','

Bad $rs in %s. Connection or SQL invalid. Try using $connection->debug=true;

'); + + // allow [ ] @ ` " and . in table names + define('ADODB_TABLE_REGEX','([]0-9a-z_\:\"\`\.\@\[-]*)'); + + // prefetching used by oracle + if (!defined('ADODB_PREFETCH_ROWS')) { + define('ADODB_PREFETCH_ROWS',10); + } + + + /** + * Fetch mode + * + * Set global variable $ADODB_FETCH_MODE to one of these constants or use + * the SetFetchMode() method to control how recordset fields are returned + * when fetching data. + * + * - NUM: array() + * - ASSOC: array('id' => 456, 'name' => 'john') + * - BOTH: array(0 => 456, 'id' => 456, 1 => 'john', 'name' => 'john') + * - DEFAULT: driver-dependent + */ + define('ADODB_FETCH_DEFAULT', 0); + define('ADODB_FETCH_NUM', 1); + define('ADODB_FETCH_ASSOC', 2); + define('ADODB_FETCH_BOTH', 3); + + /** + * Associative array case constants + * + * By defining the ADODB_ASSOC_CASE constant to one of these values, it is + * possible to control the case of field names (associative array's keys) + * when operating in ADODB_FETCH_ASSOC fetch mode. + * - LOWER: $rs->fields['orderid'] + * - UPPER: $rs->fields['ORDERID'] + * - NATIVE: $rs->fields['OrderID'] (or whatever the RDBMS will return) + * + * The default is to use native case-names. + * + * NOTE: This functionality is not implemented everywhere, it currently + * works only with: mssql, odbc, oci8 and ibase derived drivers + */ + define('ADODB_ASSOC_CASE_LOWER', 0); + define('ADODB_ASSOC_CASE_UPPER', 1); + define('ADODB_ASSOC_CASE_NATIVE', 2); + + + if (!defined('TIMESTAMP_FIRST_YEAR')) { + define('TIMESTAMP_FIRST_YEAR',100); + } + + /** + * AutoExecute constants + * (moved from adodb-pear.inc.php since they are only used in here) + */ + define('DB_AUTOQUERY_INSERT', 1); + define('DB_AUTOQUERY_UPDATE', 2); + + + // PHP's version scheme makes converting to numbers difficult - workaround + $_adodb_ver = (float) PHP_VERSION; + if ($_adodb_ver >= 5.2) { + define('ADODB_PHPVER',0x5200); + } else if ($_adodb_ver >= 5.0) { + define('ADODB_PHPVER',0x5000); + } else { + die("PHP5 or later required. You are running ".PHP_VERSION); + } + unset($_adodb_ver); + } + + + /** + Accepts $src and $dest arrays, replacing string $data + */ + function ADODB_str_replace($src, $dest, $data) { + if (ADODB_PHPVER >= 0x4050) { + return str_replace($src,$dest,$data); + } + + $s = reset($src); + $d = reset($dest); + while ($s !== false) { + $data = str_replace($s,$d,$data); + $s = next($src); + $d = next($dest); + } + return $data; + } + + function ADODB_Setup() { + GLOBAL + $ADODB_vers, // database version + $ADODB_COUNTRECS, // count number of records returned - slows down query + $ADODB_CACHE_DIR, // directory to cache recordsets + $ADODB_FETCH_MODE, + $ADODB_CACHE, + $ADODB_CACHE_CLASS, + $ADODB_FORCE_TYPE, + $ADODB_GETONE_EOF, + $ADODB_QUOTE_FIELDNAMES; + + if (empty($ADODB_CACHE_CLASS)) { + $ADODB_CACHE_CLASS = 'ADODB_Cache_File' ; + } + $ADODB_FETCH_MODE = ADODB_FETCH_DEFAULT; + $ADODB_FORCE_TYPE = ADODB_FORCE_VALUE; + $ADODB_GETONE_EOF = null; + + if (!isset($ADODB_CACHE_DIR)) { + $ADODB_CACHE_DIR = '/tmp'; //(isset($_ENV['TMP'])) ? $_ENV['TMP'] : '/tmp'; + } else { + // do not accept url based paths, eg. http:/ or ftp:/ + if (strpos($ADODB_CACHE_DIR,'://') !== false) { + die("Illegal path http:// or ftp://"); + } + } + + + // Initialize random number generator for randomizing cache flushes + // -- note Since PHP 4.2.0, the seed becomes optional and defaults to a random value if omitted. + srand(((double)microtime())*1000000); + + /** + * ADODB version as a string. + */ + $ADODB_vers = 'v5.20.9 21-Dec-2016'; + + /** + * Determines whether recordset->RecordCount() is used. + * Set to false for highest performance -- RecordCount() will always return -1 then + * for databases that provide "virtual" recordcounts... + */ + if (!isset($ADODB_COUNTRECS)) { + $ADODB_COUNTRECS = true; + } + } + + + //============================================================================================== + // CHANGE NOTHING BELOW UNLESS YOU ARE DESIGNING ADODB + //============================================================================================== + + ADODB_Setup(); + + //============================================================================================== + // CLASS ADOFieldObject + //============================================================================================== + /** + * Helper class for FetchFields -- holds info on a column + */ + class ADOFieldObject { + var $name = ''; + var $max_length=0; + var $type=""; +/* + // additional fields by dannym... (danny_milo@yahoo.com) + var $not_null = false; + // actually, this has already been built-in in the postgres, fbsql AND mysql module? ^-^ + // so we can as well make not_null standard (leaving it at "false" does not harm anyways) + + var $has_default = false; // this one I have done only in mysql and postgres for now ... + // others to come (dannym) + var $default_value; // default, if any, and supported. Check has_default first. +*/ + } + + + function _adodb_safedate($s) { + return str_replace(array("'", '\\'), '', $s); + } + + // parse date string to prevent injection attack + // date string will have one quote at beginning e.g. '3434343' + function _adodb_safedateq($s) { + $len = strlen($s); + if ($s[0] !== "'") { + $s2 = "'".$s[0]; + } else { + $s2 = "'"; + } + for($i=1; $i<$len; $i++) { + $ch = $s[$i]; + if ($ch === '\\') { + $s2 .= "'"; + break; + } elseif ($ch === "'") { + $s2 .= $ch; + break; + } + + $s2 .= $ch; + } + + return strlen($s2) == 0 ? 'null' : $s2; + } + + + // for transaction handling + + function ADODB_TransMonitor($dbms, $fn, $errno, $errmsg, $p1, $p2, &$thisConnection) { + //print "Errorno ($fn errno=$errno m=$errmsg) "; + $thisConnection->_transOK = false; + if ($thisConnection->_oldRaiseFn) { + $fn = $thisConnection->_oldRaiseFn; + $fn($dbms, $fn, $errno, $errmsg, $p1, $p2,$thisConnection); + } + } + + //------------------ + // class for caching + class ADODB_Cache_File { + + var $createdir = true; // requires creation of temp dirs + + function __construct() { + global $ADODB_INCLUDED_CSV; + if (empty($ADODB_INCLUDED_CSV)) { + include_once(ADODB_DIR.'/adodb-csvlib.inc.php'); + } + } + + // write serialised recordset to cache item/file + function writecache($filename, $contents, $debug, $secs2cache) { + return adodb_write_file($filename, $contents,$debug); + } + + // load serialised recordset and unserialise it + function &readcache($filename, &$err, $secs2cache, $rsClass) { + $rs = csv2rs($filename,$err,$secs2cache,$rsClass); + return $rs; + } + + // flush all items in cache + function flushall($debug=false) { + global $ADODB_CACHE_DIR; + + $rez = false; + + if (strlen($ADODB_CACHE_DIR) > 1) { + $rez = $this->_dirFlush($ADODB_CACHE_DIR); + if ($debug) { + ADOConnection::outp( "flushall: $ADODB_CACHE_DIR
\n". $rez."
"); + } + } + return $rez; + } + + // flush one file in cache + function flushcache($f, $debug=false) { + if (!@unlink($f)) { + if ($debug) { + ADOConnection::outp( "flushcache: failed for $f"); + } + } + } + + function getdirname($hash) { + global $ADODB_CACHE_DIR; + if (!isset($this->notSafeMode)) { + $this->notSafeMode = !ini_get('safe_mode'); + } + return ($this->notSafeMode) ? $ADODB_CACHE_DIR.'/'.substr($hash,0,2) : $ADODB_CACHE_DIR; + } + + // create temp directories + function createdir($hash, $debug) { + global $ADODB_CACHE_PERMS; + + $dir = $this->getdirname($hash); + if ($this->notSafeMode && !file_exists($dir)) { + $oldu = umask(0); + if (!@mkdir($dir, empty($ADODB_CACHE_PERMS) ? 0771 : $ADODB_CACHE_PERMS)) { + if(!is_dir($dir) && $debug) { + ADOConnection::outp("Cannot create $dir"); + } + } + umask($oldu); + } + + return $dir; + } + + /** + * Private function to erase all of the files and subdirectories in a directory. + * + * Just specify the directory, and tell it if you want to delete the directory or just clear it out. + * Note: $kill_top_level is used internally in the function to flush subdirectories. + */ + function _dirFlush($dir, $kill_top_level = false) { + if(!$dh = @opendir($dir)) return; + + while (($obj = readdir($dh))) { + if($obj=='.' || $obj=='..') continue; + $f = $dir.'/'.$obj; + + if (strpos($obj,'.cache')) { + @unlink($f); + } + if (is_dir($f)) { + $this->_dirFlush($f, true); + } + } + if ($kill_top_level === true) { + @rmdir($dir); + } + return true; + } + } + + //============================================================================================== + // CLASS ADOConnection + //============================================================================================== + + /** + * Connection object. For connecting to databases, and executing queries. + */ + abstract class ADOConnection { + // + // PUBLIC VARS + // + var $dataProvider = 'native'; + var $databaseType = ''; /// RDBMS currently in use, eg. odbc, mysql, mssql + var $database = ''; /// Name of database to be used. + var $host = ''; /// The hostname of the database server + var $user = ''; /// The username which is used to connect to the database server. + var $password = ''; /// Password for the username. For security, we no longer store it. + var $debug = false; /// if set to true will output sql statements + var $maxblobsize = 262144; /// maximum size of blobs or large text fields (262144 = 256K)-- some db's die otherwise like foxpro + var $concat_operator = '+'; /// default concat operator -- change to || for Oracle/Interbase + var $substr = 'substr'; /// substring operator + var $length = 'length'; /// string length ofperator + var $random = 'rand()'; /// random function + var $upperCase = 'upper'; /// uppercase function + var $fmtDate = "'Y-m-d'"; /// used by DBDate() as the default date format used by the database + var $fmtTimeStamp = "'Y-m-d, h:i:s A'"; /// used by DBTimeStamp as the default timestamp fmt. + var $true = '1'; /// string that represents TRUE for a database + var $false = '0'; /// string that represents FALSE for a database + var $replaceQuote = "\\'"; /// string to use to replace quotes + var $nameQuote = '"'; /// string to use to quote identifiers and names + var $charSet=false; /// character set to use - only for interbase, postgres and oci8 + var $metaDatabasesSQL = ''; + var $metaTablesSQL = ''; + var $uniqueOrderBy = false; /// All order by columns have to be unique + var $emptyDate = ' '; + var $emptyTimeStamp = ' '; + var $lastInsID = false; + //-- + var $hasInsertID = false; /// supports autoincrement ID? + var $hasAffectedRows = false; /// supports affected rows for update/delete? + var $hasTop = false; /// support mssql/access SELECT TOP 10 * FROM TABLE + var $hasLimit = false; /// support pgsql/mysql SELECT * FROM TABLE LIMIT 10 + var $readOnly = false; /// this is a readonly database - used by phpLens + var $hasMoveFirst = false; /// has ability to run MoveFirst(), scrolling backwards + var $hasGenID = false; /// can generate sequences using GenID(); + var $hasTransactions = true; /// has transactions + //-- + var $genID = 0; /// sequence id used by GenID(); + var $raiseErrorFn = false; /// error function to call + var $isoDates = false; /// accepts dates in ISO format + var $cacheSecs = 3600; /// cache for 1 hour + + // memcache + var $memCache = false; /// should we use memCache instead of caching in files + var $memCacheHost; /// memCache host + var $memCachePort = 11211; /// memCache port + var $memCacheCompress = false; /// Use 'true' to store the item compressed (uses zlib) + + var $sysDate = false; /// name of function that returns the current date + var $sysTimeStamp = false; /// name of function that returns the current timestamp + var $sysUTimeStamp = false; // name of function that returns the current timestamp accurate to the microsecond or nearest fraction + var $arrayClass = 'ADORecordSet_array'; /// name of class used to generate array recordsets, which are pre-downloaded recordsets + + var $noNullStrings = false; /// oracle specific stuff - if true ensures that '' is converted to ' ' + var $numCacheHits = 0; + var $numCacheMisses = 0; + var $pageExecuteCountRows = true; + var $uniqueSort = false; /// indicates that all fields in order by must be unique + var $leftOuter = false; /// operator to use for left outer join in WHERE clause + var $rightOuter = false; /// operator to use for right outer join in WHERE clause + var $ansiOuter = false; /// whether ansi outer join syntax supported + var $autoRollback = false; // autoRollback on PConnect(). + var $poorAffectedRows = false; // affectedRows not working or unreliable + + var $fnExecute = false; + var $fnCacheExecute = false; + var $blobEncodeType = false; // false=not required, 'I'=encode to integer, 'C'=encode to char + var $rsPrefix = "ADORecordSet_"; + + var $autoCommit = true; /// do not modify this yourself - actually private + var $transOff = 0; /// temporarily disable transactions + var $transCnt = 0; /// count of nested transactions + + var $fetchMode=false; + + var $null2null = 'null'; // in autoexecute/getinsertsql/getupdatesql, this value will be converted to a null + var $bulkBind = false; // enable 2D Execute array + // + // PRIVATE VARS + // + var $_oldRaiseFn = false; + var $_transOK = null; + var $_connectionID = false; /// The returned link identifier whenever a successful database connection is made. + var $_errorMsg = false; /// A variable which was used to keep the returned last error message. The value will + /// then returned by the errorMsg() function + var $_errorCode = false; /// Last error code, not guaranteed to be used - only by oci8 + var $_queryID = false; /// This variable keeps the last created result link identifier + + var $_isPersistentConnection = false; /// A boolean variable to state whether its a persistent connection or normal connection. */ + var $_bindInputArray = false; /// set to true if ADOConnection.Execute() permits binding of array parameters. + var $_evalAll = false; + var $_affected = false; + var $_logsql = false; + var $_transmode = ''; // transaction mode + + /* + * Additional parameters that may be passed to drivers in the connect string + * Driver must be coded to accept the parameters + */ + protected $connectionParameters = array(); + + /** + * Adds a parameter to the connection string. + * + * These parameters are added to the connection string when connecting, + * if the driver is coded to use it. + * + * @param string $parameter The name of the parameter to set + * @param string $value The value of the parameter + * + * @return null + * + * @example, for mssqlnative driver ('CharacterSet','UTF-8') + */ + final public function setConnectionParameter($parameter,$value) + { + + $this->connectionParameters[$parameter] = $value; + + } + + static function Version() { + global $ADODB_vers; + + // Semantic Version number matching regex + $regex = '^[vV]?(\d+\.\d+\.\d+' // Version number (X.Y.Z) with optional 'V' + . '(?:-(?:' // Optional preprod version: a '-' + . 'dev|' // followed by 'dev' + . '(?:(?:alpha|beta|rc)(?:\.\d+))' // or a preprod suffix and version number + . '))?)(?:\s|$)'; // Whitespace or end of string + + if (!preg_match("/$regex/", $ADODB_vers, $matches)) { + // This should normally not happen... Return whatever is between the start + // of the string and the first whitespace (or the end of the string). + self::outp("Invalid version number: '$ADODB_vers'", 'Version'); + $regex = '^[vV]?(.*?)(?:\s|$)'; + preg_match("/$regex/", $ADODB_vers, $matches); + } + return $matches[1]; + } + + /** + Get server version info... + + @returns An array with 2 elements: $arr['string'] is the description string, + and $arr[version] is the version (also a string). + */ + function ServerInfo() { + return array('description' => '', 'version' => ''); + } + + function IsConnected() { + return !empty($this->_connectionID); + } + + function _findvers($str) { + if (preg_match('/([0-9]+\.([0-9\.])+)/',$str, $arr)) { + return $arr[1]; + } else { + return ''; + } + } + + /** + * All error messages go through this bottleneck function. + * You can define your own handler by defining the function name in ADODB_OUTP. + */ + static function outp($msg,$newline=true) { + global $ADODB_FLUSH,$ADODB_OUTP; + + if (defined('ADODB_OUTP')) { + $fn = ADODB_OUTP; + $fn($msg,$newline); + return; + } else if (isset($ADODB_OUTP)) { + $fn = $ADODB_OUTP; + $fn($msg,$newline); + return; + } + + if ($newline) { + $msg .= "
\n"; + } + + if (isset($_SERVER['HTTP_USER_AGENT']) || !$newline) { + echo $msg; + } else { + echo strip_tags($msg); + } + + + if (!empty($ADODB_FLUSH) && ob_get_length() !== false) { + flush(); // do not flush if output buffering enabled - useless - thx to Jesse Mullan + } + + } + + function Time() { + $rs = $this->_Execute("select $this->sysTimeStamp"); + if ($rs && !$rs->EOF) { + return $this->UnixTimeStamp(reset($rs->fields)); + } + + return false; + } + + /** + * Connect to database + * + * @param [argHostname] Host to connect to + * @param [argUsername] Userid to login + * @param [argPassword] Associated password + * @param [argDatabaseName] database + * @param [forceNew] force new connection + * + * @return true or false + */ + function Connect($argHostname = "", $argUsername = "", $argPassword = "", $argDatabaseName = "", $forceNew = false) { + if ($argHostname != "") { + $this->host = $argHostname; + } + if ( strpos($this->host, ':') > 0 && isset($this->port) ) { + list($this->host, $this->port) = explode(":", $this->host, 2); + } + if ($argUsername != "") { + $this->user = $argUsername; + } + if ($argPassword != "") { + $this->password = 'not stored'; // not stored for security reasons + } + if ($argDatabaseName != "") { + $this->database = $argDatabaseName; + } + + $this->_isPersistentConnection = false; + + if ($forceNew) { + if ($rez=$this->_nconnect($this->host, $this->user, $argPassword, $this->database)) { + return true; + } + } else { + if ($rez=$this->_connect($this->host, $this->user, $argPassword, $this->database)) { + return true; + } + } + if (isset($rez)) { + $err = $this->ErrorMsg(); + $errno = $this->ErrorNo(); + if (empty($err)) { + $err = "Connection error to server '$argHostname' with user '$argUsername'"; + } + } else { + $err = "Missing extension for ".$this->dataProvider; + $errno = 0; + } + if ($fn = $this->raiseErrorFn) { + $fn($this->databaseType, 'CONNECT', $errno, $err, $this->host, $this->database, $this); + } + + $this->_connectionID = false; + if ($this->debug) { + ADOConnection::outp( $this->host.': '.$err); + } + return false; + } + + function _nconnect($argHostname, $argUsername, $argPassword, $argDatabaseName) { + return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabaseName); + } + + + /** + * Always force a new connection to database - currently only works with oracle + * + * @param [argHostname] Host to connect to + * @param [argUsername] Userid to login + * @param [argPassword] Associated password + * @param [argDatabaseName] database + * + * @return true or false + */ + function NConnect($argHostname = "", $argUsername = "", $argPassword = "", $argDatabaseName = "") { + return $this->Connect($argHostname, $argUsername, $argPassword, $argDatabaseName, true); + } + + /** + * Establish persistent connect to database + * + * @param [argHostname] Host to connect to + * @param [argUsername] Userid to login + * @param [argPassword] Associated password + * @param [argDatabaseName] database + * + * @return return true or false + */ + function PConnect($argHostname = "", $argUsername = "", $argPassword = "", $argDatabaseName = "") { + + if (defined('ADODB_NEVER_PERSIST')) { + return $this->Connect($argHostname,$argUsername,$argPassword,$argDatabaseName); + } + + if ($argHostname != "") { + $this->host = $argHostname; + } + if ( strpos($this->host, ':') > 0 && isset($this->port) ) { + list($this->host, $this->port) = explode(":", $this->host, 2); + } + if ($argUsername != "") { + $this->user = $argUsername; + } + if ($argPassword != "") { + $this->password = 'not stored'; + } + if ($argDatabaseName != "") { + $this->database = $argDatabaseName; + } + + $this->_isPersistentConnection = true; + + if ($rez = $this->_pconnect($this->host, $this->user, $argPassword, $this->database)) { + return true; + } + if (isset($rez)) { + $err = $this->ErrorMsg(); + if (empty($err)) { + $err = "Connection error to server '$argHostname' with user '$argUsername'"; + } + $ret = false; + } else { + $err = "Missing extension for ".$this->dataProvider; + $ret = 0; + } + if ($fn = $this->raiseErrorFn) { + $fn($this->databaseType,'PCONNECT',$this->ErrorNo(),$err,$this->host,$this->database,$this); + } + + $this->_connectionID = false; + if ($this->debug) { + ADOConnection::outp( $this->host.': '.$err); + } + return $ret; + } + + function outp_throw($msg,$src='WARN',$sql='') { + if (defined('ADODB_ERROR_HANDLER') && ADODB_ERROR_HANDLER == 'adodb_throw') { + adodb_throw($this->databaseType,$src,-9999,$msg,$sql,false,$this); + return; + } + ADOConnection::outp($msg); + } + + // create cache class. Code is backward compat with old memcache implementation + function _CreateCache() { + global $ADODB_CACHE, $ADODB_CACHE_CLASS; + + if ($this->memCache) { + global $ADODB_INCLUDED_MEMCACHE; + + if (empty($ADODB_INCLUDED_MEMCACHE)) { + include_once(ADODB_DIR.'/adodb-memcache.lib.inc.php'); + } + $ADODB_CACHE = new ADODB_Cache_MemCache($this); + } else { + $ADODB_CACHE = new $ADODB_CACHE_CLASS($this); + } + } + + // Format date column in sql string given an input format that understands Y M D + function SQLDate($fmt, $col=false) { + if (!$col) { + $col = $this->sysDate; + } + return $col; // child class implement + } + + /** + * Should prepare the sql statement and return the stmt resource. + * For databases that do not support this, we return the $sql. To ensure + * compatibility with databases that do not support prepare: + * + * $stmt = $db->Prepare("insert into table (id, name) values (?,?)"); + * $db->Execute($stmt,array(1,'Jill')) or die('insert failed'); + * $db->Execute($stmt,array(2,'Joe')) or die('insert failed'); + * + * @param sql SQL to send to database + * + * @return return FALSE, or the prepared statement, or the original sql if + * if the database does not support prepare. + * + */ + function Prepare($sql) { + return $sql; + } + + /** + * Some databases, eg. mssql require a different function for preparing + * stored procedures. So we cannot use Prepare(). + * + * Should prepare the stored procedure and return the stmt resource. + * For databases that do not support this, we return the $sql. To ensure + * compatibility with databases that do not support prepare: + * + * @param sql SQL to send to database + * + * @return return FALSE, or the prepared statement, or the original sql if + * if the database does not support prepare. + * + */ + function PrepareSP($sql,$param=true) { + return $this->Prepare($sql,$param); + } + + /** + * PEAR DB Compat + */ + function Quote($s) { + return $this->qstr($s,false); + } + + /** + * Requested by "Karsten Dambekalns" + */ + function QMagic($s) { + return $this->qstr($s,get_magic_quotes_gpc()); + } + + function q(&$s) { + //if (!empty($this->qNull && $s == 'null') { + // return $s; + //} + $s = $this->qstr($s,false); + } + + /** + * PEAR DB Compat - do not use internally. + */ + function ErrorNative() { + return $this->ErrorNo(); + } + + + /** + * PEAR DB Compat - do not use internally. + */ + function nextId($seq_name) { + return $this->GenID($seq_name); + } + + /** + * Lock a row, will escalate and lock the table if row locking not supported + * will normally free the lock at the end of the transaction + * + * @param $table name of table to lock + * @param $where where clause to use, eg: "WHERE row=12". If left empty, will escalate to table lock + */ + function RowLock($table,$where,$col='1 as adodbignore') { + return false; + } + + function CommitLock($table) { + return $this->CommitTrans(); + } + + function RollbackLock($table) { + return $this->RollbackTrans(); + } + + /** + * PEAR DB Compat - do not use internally. + * + * The fetch modes for NUMERIC and ASSOC for PEAR DB and ADODB are identical + * for easy porting :-) + * + * @param mode The fetchmode ADODB_FETCH_ASSOC or ADODB_FETCH_NUM + * @returns The previous fetch mode + */ + function SetFetchMode($mode) { + $old = $this->fetchMode; + $this->fetchMode = $mode; + + if ($old === false) { + global $ADODB_FETCH_MODE; + return $ADODB_FETCH_MODE; + } + return $old; + } + + + /** + * PEAR DB Compat - do not use internally. + */ + function Query($sql, $inputarr=false) { + $rs = $this->Execute($sql, $inputarr); + if (!$rs && defined('ADODB_PEAR')) { + return ADODB_PEAR_Error(); + } + return $rs; + } + + + /** + * PEAR DB Compat - do not use internally + */ + function LimitQuery($sql, $offset, $count, $params=false) { + $rs = $this->SelectLimit($sql, $count, $offset, $params); + if (!$rs && defined('ADODB_PEAR')) { + return ADODB_PEAR_Error(); + } + return $rs; + } + + + /** + * PEAR DB Compat - do not use internally + */ + function Disconnect() { + return $this->Close(); + } + + /** + * Returns a placeholder for query parameters + * e.g. $DB->Param('a') will return + * - '?' for most databases + * - ':a' for Oracle + * - '$1', '$2', etc. for PostgreSQL + * @param string $name parameter's name, false to force a reset of the + * number to 1 (for databases that require positioned + * params such as PostgreSQL; note that ADOdb will + * automatically reset this when executing a query ) + * @param string $type (unused) + * @return string query parameter placeholder + */ + function Param($name,$type='C') { + return '?'; + } + + /* + InParameter and OutParameter are self-documenting versions of Parameter(). + */ + function InParameter(&$stmt,&$var,$name,$maxLen=4000,$type=false) { + return $this->Parameter($stmt,$var,$name,false,$maxLen,$type); + } + + /* + */ + function OutParameter(&$stmt,&$var,$name,$maxLen=4000,$type=false) { + return $this->Parameter($stmt,$var,$name,true,$maxLen,$type); + + } + + + /* + Usage in oracle + $stmt = $db->Prepare('select * from table where id =:myid and group=:group'); + $db->Parameter($stmt,$id,'myid'); + $db->Parameter($stmt,$group,'group',64); + $db->Execute(); + + @param $stmt Statement returned by Prepare() or PrepareSP(). + @param $var PHP variable to bind to + @param $name Name of stored procedure variable name to bind to. + @param [$isOutput] Indicates direction of parameter 0/false=IN 1=OUT 2= IN/OUT. This is ignored in oci8. + @param [$maxLen] Holds an maximum length of the variable. + @param [$type] The data type of $var. Legal values depend on driver. + + */ + function Parameter(&$stmt,&$var,$name,$isOutput=false,$maxLen=4000,$type=false) { + return false; + } + + + function IgnoreErrors($saveErrs=false) { + if (!$saveErrs) { + $saveErrs = array($this->raiseErrorFn,$this->_transOK); + $this->raiseErrorFn = false; + return $saveErrs; + } else { + $this->raiseErrorFn = $saveErrs[0]; + $this->_transOK = $saveErrs[1]; + } + } + + /** + * Improved method of initiating a transaction. Used together with CompleteTrans(). + * Advantages include: + * + * a. StartTrans/CompleteTrans is nestable, unlike BeginTrans/CommitTrans/RollbackTrans. + * Only the outermost block is treated as a transaction.
+ * b. CompleteTrans auto-detects SQL errors, and will rollback on errors, commit otherwise.
+ * c. All BeginTrans/CommitTrans/RollbackTrans inside a StartTrans/CompleteTrans block + * are disabled, making it backward compatible. + */ + function StartTrans($errfn = 'ADODB_TransMonitor') { + if ($this->transOff > 0) { + $this->transOff += 1; + return true; + } + + $this->_oldRaiseFn = $this->raiseErrorFn; + $this->raiseErrorFn = $errfn; + $this->_transOK = true; + + if ($this->debug && $this->transCnt > 0) { + ADOConnection::outp("Bad Transaction: StartTrans called within BeginTrans"); + } + $ok = $this->BeginTrans(); + $this->transOff = 1; + return $ok; + } + + + /** + Used together with StartTrans() to end a transaction. Monitors connection + for sql errors, and will commit or rollback as appropriate. + + @autoComplete if true, monitor sql errors and commit and rollback as appropriate, + and if set to false force rollback even if no SQL error detected. + @returns true on commit, false on rollback. + */ + function CompleteTrans($autoComplete = true) { + if ($this->transOff > 1) { + $this->transOff -= 1; + return true; + } + $this->raiseErrorFn = $this->_oldRaiseFn; + + $this->transOff = 0; + if ($this->_transOK && $autoComplete) { + if (!$this->CommitTrans()) { + $this->_transOK = false; + if ($this->debug) { + ADOConnection::outp("Smart Commit failed"); + } + } else { + if ($this->debug) { + ADOConnection::outp("Smart Commit occurred"); + } + } + } else { + $this->_transOK = false; + $this->RollbackTrans(); + if ($this->debug) { + ADOCOnnection::outp("Smart Rollback occurred"); + } + } + + return $this->_transOK; + } + + /* + At the end of a StartTrans/CompleteTrans block, perform a rollback. + */ + function FailTrans() { + if ($this->debug) + if ($this->transOff == 0) { + ADOConnection::outp("FailTrans outside StartTrans/CompleteTrans"); + } else { + ADOConnection::outp("FailTrans was called"); + adodb_backtrace(); + } + $this->_transOK = false; + } + + /** + Check if transaction has failed, only for Smart Transactions. + */ + function HasFailedTrans() { + if ($this->transOff > 0) { + return $this->_transOK == false; + } + return false; + } + + /** + * Execute SQL + * + * @param sql SQL statement to execute, or possibly an array holding prepared statement ($sql[0] will hold sql text) + * @param [inputarr] holds the input data to bind to. Null elements will be set to null. + * @return RecordSet or false + */ + function Execute($sql,$inputarr=false) { + if ($this->fnExecute) { + $fn = $this->fnExecute; + $ret = $fn($this,$sql,$inputarr); + if (isset($ret)) { + return $ret; + } + } + if ($inputarr !== false) { + if (!is_array($inputarr)) { + $inputarr = array($inputarr); + } + + $element0 = reset($inputarr); + # is_object check because oci8 descriptors can be passed in + $array_2d = $this->bulkBind && is_array($element0) && !is_object(reset($element0)); + + //remove extra memory copy of input -mikefedyk + unset($element0); + + if (!is_array($sql) && !$this->_bindInputArray) { + // @TODO this would consider a '?' within a string as a parameter... + $sqlarr = explode('?',$sql); + $nparams = sizeof($sqlarr)-1; + + if (!$array_2d) { + // When not Bind Bulk - convert to array of arguments list + $inputarr = array($inputarr); + } else { + // Bulk bind - Make sure all list of params have the same number of elements + $countElements = array_map('count', $inputarr); + if (1 != count(array_unique($countElements))) { + $this->outp_throw( + "[bulk execute] Input array has different number of params [" . print_r($countElements, true) . "].", + 'Execute' + ); + return false; + } + unset($countElements); + } + // Make sure the number of parameters provided in the input + // array matches what the query expects + $element0 = reset($inputarr); + if ($nparams != count($element0)) { + $this->outp_throw( + "Input array has " . count($element0) . + " params, does not match query: '" . htmlspecialchars($sql) . "'", + 'Execute' + ); + return false; + } + + // clean memory + unset($element0); + + foreach($inputarr as $arr) { + $sql = ''; $i = 0; + //Use each() instead of foreach to reduce memory usage -mikefedyk + while(list(, $v) = each($arr)) { + $sql .= $sqlarr[$i]; + // from Ron Baldwin + // Only quote string types + $typ = gettype($v); + if ($typ == 'string') { + //New memory copy of input created here -mikefedyk + $sql .= $this->qstr($v); + } else if ($typ == 'double') { + $sql .= str_replace(',','.',$v); // locales fix so 1.1 does not get converted to 1,1 + } else if ($typ == 'boolean') { + $sql .= $v ? $this->true : $this->false; + } else if ($typ == 'object') { + if (method_exists($v, '__toString')) { + $sql .= $this->qstr($v->__toString()); + } else { + $sql .= $this->qstr((string) $v); + } + } else if ($v === null) { + $sql .= 'NULL'; + } else { + $sql .= $v; + } + $i += 1; + + if ($i == $nparams) { + break; + } + } // while + if (isset($sqlarr[$i])) { + $sql .= $sqlarr[$i]; + if ($i+1 != sizeof($sqlarr)) { + $this->outp_throw( "Input Array does not match ?: ".htmlspecialchars($sql),'Execute'); + } + } else if ($i != sizeof($sqlarr)) { + $this->outp_throw( "Input array does not match ?: ".htmlspecialchars($sql),'Execute'); + } + + $ret = $this->_Execute($sql); + if (!$ret) { + return $ret; + } + } + } else { + if ($array_2d) { + if (is_string($sql)) { + $stmt = $this->Prepare($sql); + } else { + $stmt = $sql; + } + + foreach($inputarr as $arr) { + $ret = $this->_Execute($stmt,$arr); + if (!$ret) { + return $ret; + } + } + } else { + $ret = $this->_Execute($sql,$inputarr); + } + } + } else { + $ret = $this->_Execute($sql,false); + } + + return $ret; + } + + function _Execute($sql,$inputarr=false) { + // ExecuteCursor() may send non-string queries (such as arrays), + // so we need to ignore those. + if( is_string($sql) ) { + // Strips keyword used to help generate SELECT COUNT(*) queries + // from SQL if it exists. + $sql = ADODB_str_replace( '_ADODB_COUNT', '', $sql ); + } + + if ($this->debug) { + global $ADODB_INCLUDED_LIB; + if (empty($ADODB_INCLUDED_LIB)) { + include(ADODB_DIR.'/adodb-lib.inc.php'); + } + $this->_queryID = _adodb_debug_execute($this, $sql,$inputarr); + } else { + $this->_queryID = @$this->_query($sql,$inputarr); + } + + // ************************ + // OK, query executed + // ************************ + + // error handling if query fails + if ($this->_queryID === false) { + if ($this->debug == 99) { + adodb_backtrace(true,5); + } + $fn = $this->raiseErrorFn; + if ($fn) { + $fn($this->databaseType,'EXECUTE',$this->ErrorNo(),$this->ErrorMsg(),$sql,$inputarr,$this); + } + return false; + } + + // return simplified recordset for inserts/updates/deletes with lower overhead + if ($this->_queryID === true) { + $rsclass = $this->rsPrefix.'empty'; + $rs = (class_exists($rsclass)) ? new $rsclass(): new ADORecordSet_empty(); + + return $rs; + } + + // return real recordset from select statement + $rsclass = $this->rsPrefix.$this->databaseType; + $rs = new $rsclass($this->_queryID,$this->fetchMode); + $rs->connection = $this; // Pablo suggestion + $rs->Init(); + if (is_array($sql)) { + $rs->sql = $sql[0]; + } else { + $rs->sql = $sql; + } + if ($rs->_numOfRows <= 0) { + global $ADODB_COUNTRECS; + if ($ADODB_COUNTRECS) { + if (!$rs->EOF) { + $rs = $this->_rs2rs($rs,-1,-1,!is_array($sql)); + $rs->_queryID = $this->_queryID; + } else + $rs->_numOfRows = 0; + } + } + return $rs; + } + + function CreateSequence($seqname='adodbseq',$startID=1) { + if (empty($this->_genSeqSQL)) { + return false; + } + return $this->Execute(sprintf($this->_genSeqSQL,$seqname,$startID)); + } + + function DropSequence($seqname='adodbseq') { + if (empty($this->_dropSeqSQL)) { + return false; + } + return $this->Execute(sprintf($this->_dropSeqSQL,$seqname)); + } + + /** + * Generates a sequence id and stores it in $this->genID; + * GenID is only available if $this->hasGenID = true; + * + * @param seqname name of sequence to use + * @param startID if sequence does not exist, start at this ID + * @return 0 if not supported, otherwise a sequence id + */ + function GenID($seqname='adodbseq',$startID=1) { + if (!$this->hasGenID) { + return 0; // formerly returns false pre 1.60 + } + + $getnext = sprintf($this->_genIDSQL,$seqname); + + $holdtransOK = $this->_transOK; + + $save_handler = $this->raiseErrorFn; + $this->raiseErrorFn = ''; + @($rs = $this->Execute($getnext)); + $this->raiseErrorFn = $save_handler; + + if (!$rs) { + $this->_transOK = $holdtransOK; //if the status was ok before reset + $createseq = $this->Execute(sprintf($this->_genSeqSQL,$seqname,$startID)); + $rs = $this->Execute($getnext); + } + if ($rs && !$rs->EOF) { + $this->genID = reset($rs->fields); + } else { + $this->genID = 0; // false + } + + if ($rs) { + $rs->Close(); + } + + return $this->genID; + } + + /** + * @param $table string name of the table, not needed by all databases (eg. mysql), default '' + * @param $column string name of the column, not needed by all databases (eg. mysql), default '' + * @return the last inserted ID. Not all databases support this. + */ + function Insert_ID($table='',$column='') { + if ($this->_logsql && $this->lastInsID) { + return $this->lastInsID; + } + if ($this->hasInsertID) { + return $this->_insertid($table,$column); + } + if ($this->debug) { + ADOConnection::outp( '

Insert_ID error

'); + adodb_backtrace(); + } + return false; + } + + + /** + * Portable Insert ID. Pablo Roca + * + * @return the last inserted ID. All databases support this. But aware possible + * problems in multiuser environments. Heavy test this before deploying. + */ + function PO_Insert_ID($table="", $id="") { + if ($this->hasInsertID){ + return $this->Insert_ID($table,$id); + } else { + return $this->GetOne("SELECT MAX($id) FROM $table"); + } + } + + /** + * @return # rows affected by UPDATE/DELETE + */ + function Affected_Rows() { + if ($this->hasAffectedRows) { + if ($this->fnExecute === 'adodb_log_sql') { + if ($this->_logsql && $this->_affected !== false) { + return $this->_affected; + } + } + $val = $this->_affectedrows(); + return ($val < 0) ? false : $val; + } + + if ($this->debug) { + ADOConnection::outp( '

Affected_Rows error

',false); + } + return false; + } + + + /** + * @return the last error message + */ + function ErrorMsg() { + if ($this->_errorMsg) { + return '!! '.strtoupper($this->dataProvider.' '.$this->databaseType).': '.$this->_errorMsg; + } else { + return ''; + } + } + + + /** + * @return the last error number. Normally 0 means no error. + */ + function ErrorNo() { + return ($this->_errorMsg) ? -1 : 0; + } + + function MetaError($err=false) { + include_once(ADODB_DIR."/adodb-error.inc.php"); + if ($err === false) { + $err = $this->ErrorNo(); + } + return adodb_error($this->dataProvider,$this->databaseType,$err); + } + + function MetaErrorMsg($errno) { + include_once(ADODB_DIR."/adodb-error.inc.php"); + return adodb_errormsg($errno); + } + + /** + * @returns an array with the primary key columns in it. + */ + function MetaPrimaryKeys($table, $owner=false) { + // owner not used in base class - see oci8 + $p = array(); + $objs = $this->MetaColumns($table); + if ($objs) { + foreach($objs as $v) { + if (!empty($v->primary_key)) { + $p[] = $v->name; + } + } + } + if (sizeof($p)) { + return $p; + } + if (function_exists('ADODB_VIEW_PRIMARYKEYS')) { + return ADODB_VIEW_PRIMARYKEYS($this->databaseType, $this->database, $table, $owner); + } + return false; + } + + /** + * @returns assoc array where keys are tables, and values are foreign keys + */ + function MetaForeignKeys($table, $owner=false, $upper=false) { + return false; + } + /** + * Choose a database to connect to. Many databases do not support this. + * + * @param dbName is the name of the database to select + * @return true or false + */ + function SelectDB($dbName) {return false;} + + + /** + * Will select, getting rows from $offset (1-based), for $nrows. + * This simulates the MySQL "select * from table limit $offset,$nrows" , and + * the PostgreSQL "select * from table limit $nrows offset $offset". Note that + * MySQL and PostgreSQL parameter ordering is the opposite of the other. + * eg. + * SelectLimit('select * from table',3); will return rows 1 to 3 (1-based) + * SelectLimit('select * from table',3,2); will return rows 3 to 5 (1-based) + * + * Uses SELECT TOP for Microsoft databases (when $this->hasTop is set) + * BUG: Currently SelectLimit fails with $sql with LIMIT or TOP clause already set + * + * @param sql + * @param [offset] is the row to start calculations from (1-based) + * @param [nrows] is the number of rows to get + * @param [inputarr] array of bind variables + * @param [secs2cache] is a private parameter only used by jlim + * @return the recordset ($rs->databaseType == 'array') + */ + function SelectLimit($sql,$nrows=-1,$offset=-1, $inputarr=false,$secs2cache=0) { + if ($this->hasTop && $nrows > 0) { + // suggested by Reinhard Balling. Access requires top after distinct + // Informix requires first before distinct - F Riosa + $ismssql = (strpos($this->databaseType,'mssql') !== false); + if ($ismssql) { + $isaccess = false; + } else { + $isaccess = (strpos($this->databaseType,'access') !== false); + } + + if ($offset <= 0) { + // access includes ties in result + if ($isaccess) { + $sql = preg_replace( + '/(^\s*select\s+(distinctrow|distinct)?)/i','\\1 '.$this->hasTop.' '.((integer)$nrows).' ',$sql); + + if ($secs2cache != 0) { + $ret = $this->CacheExecute($secs2cache, $sql,$inputarr); + } else { + $ret = $this->Execute($sql,$inputarr); + } + return $ret; // PHP5 fix + } else if ($ismssql){ + $sql = preg_replace( + '/(^\s*select\s+(distinctrow|distinct)?)/i','\\1 '.$this->hasTop.' '.((integer)$nrows).' ',$sql); + } else { + $sql = preg_replace( + '/(^\s*select\s)/i','\\1 '.$this->hasTop.' '.((integer)$nrows).' ',$sql); + } + } else { + $nn = $nrows + $offset; + if ($isaccess || $ismssql) { + $sql = preg_replace( + '/(^\s*select\s+(distinctrow|distinct)?)/i','\\1 '.$this->hasTop.' '.$nn.' ',$sql); + } else { + $sql = preg_replace( + '/(^\s*select\s)/i','\\1 '.$this->hasTop.' '.$nn.' ',$sql); + } + } + } + + // if $offset>0, we want to skip rows, and $ADODB_COUNTRECS is set, we buffer rows + // 0 to offset-1 which will be discarded anyway. So we disable $ADODB_COUNTRECS. + global $ADODB_COUNTRECS; + + $savec = $ADODB_COUNTRECS; + $ADODB_COUNTRECS = false; + + + if ($secs2cache != 0) { + $rs = $this->CacheExecute($secs2cache,$sql,$inputarr); + } else { + $rs = $this->Execute($sql,$inputarr); + } + + $ADODB_COUNTRECS = $savec; + if ($rs && !$rs->EOF) { + $rs = $this->_rs2rs($rs,$nrows,$offset); + } + //print_r($rs); + return $rs; + } + + /** + * Create serializable recordset. Breaks rs link to connection. + * + * @param rs the recordset to serialize + */ + function SerializableRS(&$rs) { + $rs2 = $this->_rs2rs($rs); + $ignore = false; + $rs2->connection = $ignore; + + return $rs2; + } + + /** + * Convert database recordset to an array recordset + * input recordset's cursor should be at beginning, and + * old $rs will be closed. + * + * @param rs the recordset to copy + * @param [nrows] number of rows to retrieve (optional) + * @param [offset] offset by number of rows (optional) + * @return the new recordset + */ + function &_rs2rs(&$rs,$nrows=-1,$offset=-1,$close=true) { + if (! $rs) { + return false; + } + $dbtype = $rs->databaseType; + if (!$dbtype) { + $rs = $rs; // required to prevent crashing in 4.2.1, but does not happen in 4.3.1 -- why ? + return $rs; + } + if (($dbtype == 'array' || $dbtype == 'csv') && $nrows == -1 && $offset == -1) { + $rs->MoveFirst(); + $rs = $rs; // required to prevent crashing in 4.2.1, but does not happen in 4.3.1-- why ? + return $rs; + } + $flds = array(); + for ($i=0, $max=$rs->FieldCount(); $i < $max; $i++) { + $flds[] = $rs->FetchField($i); + } + + $arr = $rs->GetArrayLimit($nrows,$offset); + //print_r($arr); + if ($close) { + $rs->Close(); + } + + $arrayClass = $this->arrayClass; + + $rs2 = new $arrayClass(); + $rs2->connection = $this; + $rs2->sql = $rs->sql; + $rs2->dataProvider = $this->dataProvider; + $rs2->InitArrayFields($arr,$flds); + $rs2->fetchMode = isset($rs->adodbFetchMode) ? $rs->adodbFetchMode : $rs->fetchMode; + return $rs2; + } + + /* + * Return all rows. Compat with PEAR DB + */ + function GetAll($sql, $inputarr=false) { + $arr = $this->GetArray($sql,$inputarr); + return $arr; + } + + function GetAssoc($sql, $inputarr=false,$force_array = false, $first2cols = false) { + $rs = $this->Execute($sql, $inputarr); + if (!$rs) { + return false; + } + $arr = $rs->GetAssoc($force_array,$first2cols); + return $arr; + } + + function CacheGetAssoc($secs2cache, $sql=false, $inputarr=false,$force_array = false, $first2cols = false) { + if (!is_numeric($secs2cache)) { + $first2cols = $force_array; + $force_array = $inputarr; + } + $rs = $this->CacheExecute($secs2cache, $sql, $inputarr); + if (!$rs) { + return false; + } + $arr = $rs->GetAssoc($force_array,$first2cols); + return $arr; + } + + /** + * Return first element of first row of sql statement. Recordset is disposed + * for you. + * + * @param sql SQL statement + * @param [inputarr] input bind array + */ + function GetOne($sql,$inputarr=false) { + global $ADODB_COUNTRECS,$ADODB_GETONE_EOF; + + $crecs = $ADODB_COUNTRECS; + $ADODB_COUNTRECS = false; + + $ret = false; + $rs = $this->Execute($sql,$inputarr); + if ($rs) { + if ($rs->EOF) { + $ret = $ADODB_GETONE_EOF; + } else { + $ret = reset($rs->fields); + } + + $rs->Close(); + } + $ADODB_COUNTRECS = $crecs; + return $ret; + } + + // $where should include 'WHERE fld=value' + function GetMedian($table, $field,$where = '') { + $total = $this->GetOne("select count(*) from $table $where"); + if (!$total) { + return false; + } + + $midrow = (integer) ($total/2); + $rs = $this->SelectLimit("select $field from $table $where order by 1",1,$midrow); + if ($rs && !$rs->EOF) { + return reset($rs->fields); + } + return false; + } + + + function CacheGetOne($secs2cache,$sql=false,$inputarr=false) { + global $ADODB_GETONE_EOF; + + $ret = false; + $rs = $this->CacheExecute($secs2cache,$sql,$inputarr); + if ($rs) { + if ($rs->EOF) { + $ret = $ADODB_GETONE_EOF; + } else { + $ret = reset($rs->fields); + } + $rs->Close(); + } + + return $ret; + } + + function GetCol($sql, $inputarr = false, $trim = false) { + + $rs = $this->Execute($sql, $inputarr); + if ($rs) { + $rv = array(); + if ($trim) { + while (!$rs->EOF) { + $rv[] = trim(reset($rs->fields)); + $rs->MoveNext(); + } + } else { + while (!$rs->EOF) { + $rv[] = reset($rs->fields); + $rs->MoveNext(); + } + } + $rs->Close(); + } else { + $rv = false; + } + return $rv; + } + + function CacheGetCol($secs, $sql = false, $inputarr = false,$trim=false) { + $rs = $this->CacheExecute($secs, $sql, $inputarr); + if ($rs) { + $rv = array(); + if ($trim) { + while (!$rs->EOF) { + $rv[] = trim(reset($rs->fields)); + $rs->MoveNext(); + } + } else { + while (!$rs->EOF) { + $rv[] = reset($rs->fields); + $rs->MoveNext(); + } + } + $rs->Close(); + } else + $rv = false; + + return $rv; + } + + function Transpose(&$rs,$addfieldnames=true) { + $rs2 = $this->_rs2rs($rs); + if (!$rs2) { + return false; + } + + $rs2->_transpose($addfieldnames); + return $rs2; + } + + /* + Calculate the offset of a date for a particular database and generate + appropriate SQL. Useful for calculating future/past dates and storing + in a database. + + If dayFraction=1.5 means 1.5 days from now, 1.0/24 for 1 hour. + */ + function OffsetDate($dayFraction,$date=false) { + if (!$date) { + $date = $this->sysDate; + } + return '('.$date.'+'.$dayFraction.')'; + } + + + /** + * + * @param sql SQL statement + * @param [inputarr] input bind array + */ + function GetArray($sql,$inputarr=false) { + global $ADODB_COUNTRECS; + + $savec = $ADODB_COUNTRECS; + $ADODB_COUNTRECS = false; + $rs = $this->Execute($sql,$inputarr); + $ADODB_COUNTRECS = $savec; + if (!$rs) + if (defined('ADODB_PEAR')) { + $cls = ADODB_PEAR_Error(); + return $cls; + } else { + return false; + } + $arr = $rs->GetArray(); + $rs->Close(); + return $arr; + } + + function CacheGetAll($secs2cache,$sql=false,$inputarr=false) { + $arr = $this->CacheGetArray($secs2cache,$sql,$inputarr); + return $arr; + } + + function CacheGetArray($secs2cache,$sql=false,$inputarr=false) { + global $ADODB_COUNTRECS; + + $savec = $ADODB_COUNTRECS; + $ADODB_COUNTRECS = false; + $rs = $this->CacheExecute($secs2cache,$sql,$inputarr); + $ADODB_COUNTRECS = $savec; + + if (!$rs) + if (defined('ADODB_PEAR')) { + $cls = ADODB_PEAR_Error(); + return $cls; + } else { + return false; + } + $arr = $rs->GetArray(); + $rs->Close(); + return $arr; + } + + function GetRandRow($sql, $arr= false) { + $rezarr = $this->GetAll($sql, $arr); + $sz = sizeof($rezarr); + return $rezarr[abs(rand()) % $sz]; + } + + /** + * Return one row of sql statement. Recordset is disposed for you. + * Note that SelectLimit should not be called. + * + * @param sql SQL statement + * @param [inputarr] input bind array + */ + function GetRow($sql,$inputarr=false) { + global $ADODB_COUNTRECS; + + $crecs = $ADODB_COUNTRECS; + $ADODB_COUNTRECS = false; + + $rs = $this->Execute($sql,$inputarr); + + $ADODB_COUNTRECS = $crecs; + if ($rs) { + if (!$rs->EOF) { + $arr = $rs->fields; + } else { + $arr = array(); + } + $rs->Close(); + return $arr; + } + + return false; + } + + function CacheGetRow($secs2cache,$sql=false,$inputarr=false) { + $rs = $this->CacheExecute($secs2cache,$sql,$inputarr); + if ($rs) { + if (!$rs->EOF) { + $arr = $rs->fields; + } else { + $arr = array(); + } + + $rs->Close(); + return $arr; + } + return false; + } + + /** + * Insert or replace a single record. Note: this is not the same as MySQL's replace. + * ADOdb's Replace() uses update-insert semantics, not insert-delete-duplicates of MySQL. + * Also note that no table locking is done currently, so it is possible that the + * record be inserted twice by two programs... + * + * $this->Replace('products', array('prodname' =>"'Nails'","price" => 3.99), 'prodname'); + * + * $table table name + * $fieldArray associative array of data (you must quote strings yourself). + * $keyCol the primary key field name or if compound key, array of field names + * autoQuote set to true to use a hueristic to quote strings. Works with nulls and numbers + * but does not work with dates nor SQL functions. + * has_autoinc the primary key is an auto-inc field, so skip in insert. + * + * Currently blob replace not supported + * + * returns 0 = fail, 1 = update, 2 = insert + */ + + function Replace($table, $fieldArray, $keyCol, $autoQuote=false, $has_autoinc=false) { + global $ADODB_INCLUDED_LIB; + if (empty($ADODB_INCLUDED_LIB)) { + include(ADODB_DIR.'/adodb-lib.inc.php'); + } + + return _adodb_replace($this, $table, $fieldArray, $keyCol, $autoQuote, $has_autoinc); + } + + + /** + * Will select, getting rows from $offset (1-based), for $nrows. + * This simulates the MySQL "select * from table limit $offset,$nrows" , and + * the PostgreSQL "select * from table limit $nrows offset $offset". Note that + * MySQL and PostgreSQL parameter ordering is the opposite of the other. + * eg. + * CacheSelectLimit(15,'select * from table',3); will return rows 1 to 3 (1-based) + * CacheSelectLimit(15,'select * from table',3,2); will return rows 3 to 5 (1-based) + * + * BUG: Currently CacheSelectLimit fails with $sql with LIMIT or TOP clause already set + * + * @param [secs2cache] seconds to cache data, set to 0 to force query. This is optional + * @param sql + * @param [offset] is the row to start calculations from (1-based) + * @param [nrows] is the number of rows to get + * @param [inputarr] array of bind variables + * @return the recordset ($rs->databaseType == 'array') + */ + function CacheSelectLimit($secs2cache,$sql,$nrows=-1,$offset=-1,$inputarr=false) { + if (!is_numeric($secs2cache)) { + if ($sql === false) { + $sql = -1; + } + if ($offset == -1) { + $offset = false; + } + // sql, nrows, offset,inputarr + $rs = $this->SelectLimit($secs2cache,$sql,$nrows,$offset,$this->cacheSecs); + } else { + if ($sql === false) { + $this->outp_throw("Warning: \$sql missing from CacheSelectLimit()",'CacheSelectLimit'); + } + $rs = $this->SelectLimit($sql,$nrows,$offset,$inputarr,$secs2cache); + } + return $rs; + } + + /** + * Flush cached recordsets that match a particular $sql statement. + * If $sql == false, then we purge all files in the cache. + */ + function CacheFlush($sql=false,$inputarr=false) { + global $ADODB_CACHE_DIR, $ADODB_CACHE; + + # Create cache if it does not exist + if (empty($ADODB_CACHE)) { + $this->_CreateCache(); + } + + if (!$sql) { + $ADODB_CACHE->flushall($this->debug); + return; + } + + $f = $this->_gencachename($sql.serialize($inputarr),false); + return $ADODB_CACHE->flushcache($f, $this->debug); + } + + + /** + * Private function to generate filename for caching. + * Filename is generated based on: + * + * - sql statement + * - database type (oci8, ibase, ifx, etc) + * - database name + * - userid + * - setFetchMode (adodb 4.23) + * + * When not in safe mode, we create 256 sub-directories in the cache directory ($ADODB_CACHE_DIR). + * Assuming that we can have 50,000 files per directory with good performance, + * then we can scale to 12.8 million unique cached recordsets. Wow! + */ + function _gencachename($sql,$createdir) { + global $ADODB_CACHE, $ADODB_CACHE_DIR; + + if ($this->fetchMode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } else { + $mode = $this->fetchMode; + } + $m = md5($sql.$this->databaseType.$this->database.$this->user.$mode); + if (!$ADODB_CACHE->createdir) { + return $m; + } + if (!$createdir) { + $dir = $ADODB_CACHE->getdirname($m); + } else { + $dir = $ADODB_CACHE->createdir($m, $this->debug); + } + + return $dir.'/adodb_'.$m.'.cache'; + } + + + /** + * Execute SQL, caching recordsets. + * + * @param [secs2cache] seconds to cache data, set to 0 to force query. + * This is an optional parameter. + * @param sql SQL statement to execute + * @param [inputarr] holds the input data to bind to + * @return RecordSet or false + */ + function CacheExecute($secs2cache,$sql=false,$inputarr=false) { + global $ADODB_CACHE; + + if (empty($ADODB_CACHE)) { + $this->_CreateCache(); + } + + if (!is_numeric($secs2cache)) { + $inputarr = $sql; + $sql = $secs2cache; + $secs2cache = $this->cacheSecs; + } + + if (is_array($sql)) { + $sqlparam = $sql; + $sql = $sql[0]; + } else + $sqlparam = $sql; + + + $md5file = $this->_gencachename($sql.serialize($inputarr),true); + $err = ''; + + if ($secs2cache > 0){ + $rs = $ADODB_CACHE->readcache($md5file,$err,$secs2cache,$this->arrayClass); + $this->numCacheHits += 1; + } else { + $err='Timeout 1'; + $rs = false; + $this->numCacheMisses += 1; + } + + if (!$rs) { + // no cached rs found + if ($this->debug) { + if (get_magic_quotes_runtime() && !$this->memCache) { + ADOConnection::outp("Please disable magic_quotes_runtime - it corrupts cache files :("); + } + if ($this->debug !== -1) { + ADOConnection::outp( " $md5file cache failure: $err (this is a notice and not an error)"); + } + } + + $rs = $this->Execute($sqlparam,$inputarr); + + if ($rs) { + $eof = $rs->EOF; + $rs = $this->_rs2rs($rs); // read entire recordset into memory immediately + $rs->timeCreated = time(); // used by caching + $txt = _rs2serialize($rs,false,$sql); // serialize + + $ok = $ADODB_CACHE->writecache($md5file,$txt,$this->debug, $secs2cache); + if (!$ok) { + if ($ok === false) { + $em = 'Cache write error'; + $en = -32000; + + if ($fn = $this->raiseErrorFn) { + $fn($this->databaseType,'CacheExecute', $en, $em, $md5file,$sql,$this); + } + } else { + $em = 'Cache file locked warning'; + $en = -32001; + // do not call error handling for just a warning + } + + if ($this->debug) { + ADOConnection::outp( " ".$em); + } + } + if ($rs->EOF && !$eof) { + $rs->MoveFirst(); + //$rs = csv2rs($md5file,$err); + $rs->connection = $this; // Pablo suggestion + } + + } else if (!$this->memCache) { + $ADODB_CACHE->flushcache($md5file); + } + } else { + $this->_errorMsg = ''; + $this->_errorCode = 0; + + if ($this->fnCacheExecute) { + $fn = $this->fnCacheExecute; + $fn($this, $secs2cache, $sql, $inputarr); + } + // ok, set cached object found + $rs->connection = $this; // Pablo suggestion + if ($this->debug){ + if ($this->debug == 99) { + adodb_backtrace(); + } + $inBrowser = isset($_SERVER['HTTP_USER_AGENT']); + $ttl = $rs->timeCreated + $secs2cache - time(); + $s = is_array($sql) ? $sql[0] : $sql; + if ($inBrowser) { + $s = ''.htmlspecialchars($s).''; + } + + ADOConnection::outp( " $md5file reloaded, ttl=$ttl [ $s ]"); + } + } + return $rs; + } + + + /* + Similar to PEAR DB's autoExecute(), except that + $mode can be 'INSERT' or 'UPDATE' or DB_AUTOQUERY_INSERT or DB_AUTOQUERY_UPDATE + If $mode == 'UPDATE', then $where is compulsory as a safety measure. + + $forceUpdate means that even if the data has not changed, perform update. + */ + function AutoExecute($table, $fields_values, $mode = 'INSERT', $where = false, $forceUpdate = true, $magicq = false) { + if ($where === false && ($mode == 'UPDATE' || $mode == 2 /* DB_AUTOQUERY_UPDATE */) ) { + $this->outp_throw('AutoExecute: Illegal mode=UPDATE with empty WHERE clause', 'AutoExecute'); + return false; + } + + $sql = "SELECT * FROM $table"; + $rs = $this->SelectLimit($sql, 1); + if (!$rs) { + return false; // table does not exist + } + + $rs->tableName = $table; + if ($where !== false) { + $sql .= " WHERE $where"; + } + $rs->sql = $sql; + + switch($mode) { + case 'UPDATE': + case DB_AUTOQUERY_UPDATE: + $sql = $this->GetUpdateSQL($rs, $fields_values, $forceUpdate, $magicq); + break; + case 'INSERT': + case DB_AUTOQUERY_INSERT: + $sql = $this->GetInsertSQL($rs, $fields_values, $magicq); + break; + default: + $this->outp_throw("AutoExecute: Unknown mode=$mode", 'AutoExecute'); + return false; + } + return $sql && $this->Execute($sql); + } + + + /** + * Generates an Update Query based on an existing recordset. + * $arrFields is an associative array of fields with the value + * that should be assigned. + * + * Note: This function should only be used on a recordset + * that is run against a single table and sql should only + * be a simple select stmt with no groupby/orderby/limit + * + * "Jonathan Younger" + */ + function GetUpdateSQL(&$rs, $arrFields,$forceUpdate=false,$magicq=false,$force=null) { + global $ADODB_INCLUDED_LIB; + + // ******************************************************** + // This is here to maintain compatibility + // with older adodb versions. Sets force type to force nulls if $forcenulls is set. + if (!isset($force)) { + global $ADODB_FORCE_TYPE; + $force = $ADODB_FORCE_TYPE; + } + // ******************************************************** + + if (empty($ADODB_INCLUDED_LIB)) { + include(ADODB_DIR.'/adodb-lib.inc.php'); + } + return _adodb_getupdatesql($this,$rs,$arrFields,$forceUpdate,$magicq,$force); + } + + /** + * Generates an Insert Query based on an existing recordset. + * $arrFields is an associative array of fields with the value + * that should be assigned. + * + * Note: This function should only be used on a recordset + * that is run against a single table. + */ + function GetInsertSQL(&$rs, $arrFields,$magicq=false,$force=null) { + global $ADODB_INCLUDED_LIB; + if (!isset($force)) { + global $ADODB_FORCE_TYPE; + $force = $ADODB_FORCE_TYPE; + } + if (empty($ADODB_INCLUDED_LIB)) { + include(ADODB_DIR.'/adodb-lib.inc.php'); + } + return _adodb_getinsertsql($this,$rs,$arrFields,$magicq,$force); + } + + + /** + * Update a blob column, given a where clause. There are more sophisticated + * blob handling functions that we could have implemented, but all require + * a very complex API. Instead we have chosen something that is extremely + * simple to understand and use. + * + * Note: $blobtype supports 'BLOB' and 'CLOB', default is BLOB of course. + * + * Usage to update a $blobvalue which has a primary key blob_id=1 into a + * field blobtable.blobcolumn: + * + * UpdateBlob('blobtable', 'blobcolumn', $blobvalue, 'blob_id=1'); + * + * Insert example: + * + * $conn->Execute('INSERT INTO blobtable (id, blobcol) VALUES (1, null)'); + * $conn->UpdateBlob('blobtable','blobcol',$blob,'id=1'); + */ + function UpdateBlob($table,$column,$val,$where,$blobtype='BLOB') { + return $this->Execute("UPDATE $table SET $column=? WHERE $where",array($val)) != false; + } + + /** + * Usage: + * UpdateBlob('TABLE', 'COLUMN', '/path/to/file', 'ID=1'); + * + * $blobtype supports 'BLOB' and 'CLOB' + * + * $conn->Execute('INSERT INTO blobtable (id, blobcol) VALUES (1, null)'); + * $conn->UpdateBlob('blobtable','blobcol',$blobpath,'id=1'); + */ + function UpdateBlobFile($table,$column,$path,$where,$blobtype='BLOB') { + $fd = fopen($path,'rb'); + if ($fd === false) { + return false; + } + $val = fread($fd,filesize($path)); + fclose($fd); + return $this->UpdateBlob($table,$column,$val,$where,$blobtype); + } + + function BlobDecode($blob) { + return $blob; + } + + function BlobEncode($blob) { + return $blob; + } + + function GetCharSet() { + return $this->charSet; + } + + function SetCharSet($charset) { + $this->charSet = $charset; + return true; + } + + function IfNull( $field, $ifNull ) { + return " CASE WHEN $field is null THEN $ifNull ELSE $field END "; + } + + function LogSQL($enable=true) { + include_once(ADODB_DIR.'/adodb-perf.inc.php'); + + if ($enable) { + $this->fnExecute = 'adodb_log_sql'; + } else { + $this->fnExecute = false; + } + + $old = $this->_logsql; + $this->_logsql = $enable; + if ($enable && !$old) { + $this->_affected = false; + } + return $old; + } + + /** + * Usage: + * UpdateClob('TABLE', 'COLUMN', $var, 'ID=1', 'CLOB'); + * + * $conn->Execute('INSERT INTO clobtable (id, clobcol) VALUES (1, null)'); + * $conn->UpdateClob('clobtable','clobcol',$clob,'id=1'); + */ + function UpdateClob($table,$column,$val,$where) { + return $this->UpdateBlob($table,$column,$val,$where,'CLOB'); + } + + // not the fastest implementation - quick and dirty - jlim + // for best performance, use the actual $rs->MetaType(). + function MetaType($t,$len=-1,$fieldobj=false) { + + if (empty($this->_metars)) { + $rsclass = $this->rsPrefix.$this->databaseType; + $this->_metars = new $rsclass(false,$this->fetchMode); + $this->_metars->connection = $this; + } + return $this->_metars->MetaType($t,$len,$fieldobj); + } + + + /** + * Change the SQL connection locale to a specified locale. + * This is used to get the date formats written depending on the client locale. + */ + function SetDateLocale($locale = 'En') { + $this->locale = $locale; + switch (strtoupper($locale)) + { + case 'EN': + $this->fmtDate="'Y-m-d'"; + $this->fmtTimeStamp = "'Y-m-d H:i:s'"; + break; + + case 'US': + $this->fmtDate = "'m-d-Y'"; + $this->fmtTimeStamp = "'m-d-Y H:i:s'"; + break; + + case 'PT_BR': + case 'NL': + case 'FR': + case 'RO': + case 'IT': + $this->fmtDate="'d-m-Y'"; + $this->fmtTimeStamp = "'d-m-Y H:i:s'"; + break; + + case 'GE': + $this->fmtDate="'d.m.Y'"; + $this->fmtTimeStamp = "'d.m.Y H:i:s'"; + break; + + default: + $this->fmtDate="'Y-m-d'"; + $this->fmtTimeStamp = "'Y-m-d H:i:s'"; + break; + } + } + + /** + * GetActiveRecordsClass Performs an 'ALL' query + * + * @param mixed $class This string represents the class of the current active record + * @param mixed $table Table used by the active record object + * @param mixed $whereOrderBy Where, order, by clauses + * @param mixed $bindarr + * @param mixed $primkeyArr + * @param array $extra Query extras: limit, offset... + * @param mixed $relations Associative array: table's foreign name, "hasMany", "belongsTo" + * @access public + * @return void + */ + function GetActiveRecordsClass( + $class, $table,$whereOrderBy=false,$bindarr=false, $primkeyArr=false, + $extra=array(), + $relations=array()) + { + global $_ADODB_ACTIVE_DBS; + ## reduce overhead of adodb.inc.php -- moved to adodb-active-record.inc.php + ## if adodb-active-recordx is loaded -- should be no issue as they will probably use Find() + if (!isset($_ADODB_ACTIVE_DBS)) { + include_once(ADODB_DIR.'/adodb-active-record.inc.php'); + } + return adodb_GetActiveRecordsClass($this, $class, $table, $whereOrderBy, $bindarr, $primkeyArr, $extra, $relations); + } + + function GetActiveRecords($table,$where=false,$bindarr=false,$primkeyArr=false) { + $arr = $this->GetActiveRecordsClass('ADODB_Active_Record', $table, $where, $bindarr, $primkeyArr); + return $arr; + } + + /** + * Close Connection + */ + function Close() { + $rez = $this->_close(); + $this->_connectionID = false; + return $rez; + } + + /** + * Begin a Transaction. Must be followed by CommitTrans() or RollbackTrans(). + * + * @return true if succeeded or false if database does not support transactions + */ + function BeginTrans() { + if ($this->debug) { + ADOConnection::outp("BeginTrans: Transactions not supported for this driver"); + } + return false; + } + + /* set transaction mode */ + function SetTransactionMode( $transaction_mode ) { + $transaction_mode = $this->MetaTransaction($transaction_mode, $this->dataProvider); + $this->_transmode = $transaction_mode; + } +/* +http://msdn2.microsoft.com/en-US/ms173763.aspx +http://dev.mysql.com/doc/refman/5.0/en/innodb-transaction-isolation.html +http://www.postgresql.org/docs/8.1/interactive/sql-set-transaction.html +http://www.stanford.edu/dept/itss/docs/oracle/10g/server.101/b10759/statements_10005.htm +*/ + function MetaTransaction($mode,$db) { + $mode = strtoupper($mode); + $mode = str_replace('ISOLATION LEVEL ','',$mode); + + switch($mode) { + + case 'READ UNCOMMITTED': + switch($db) { + case 'oci8': + case 'oracle': + return 'ISOLATION LEVEL READ COMMITTED'; + default: + return 'ISOLATION LEVEL READ UNCOMMITTED'; + } + break; + + case 'READ COMMITTED': + return 'ISOLATION LEVEL READ COMMITTED'; + break; + + case 'REPEATABLE READ': + switch($db) { + case 'oci8': + case 'oracle': + return 'ISOLATION LEVEL SERIALIZABLE'; + default: + return 'ISOLATION LEVEL REPEATABLE READ'; + } + break; + + case 'SERIALIZABLE': + return 'ISOLATION LEVEL SERIALIZABLE'; + break; + + default: + return $mode; + } + } + + /** + * If database does not support transactions, always return true as data always commited + * + * @param $ok set to false to rollback transaction, true to commit + * + * @return true/false. + */ + function CommitTrans($ok=true) { + return true; + } + + + /** + * If database does not support transactions, rollbacks always fail, so return false + * + * @return true/false. + */ + function RollbackTrans() { + return false; + } + + + /** + * return the databases that the driver can connect to. + * Some databases will return an empty array. + * + * @return an array of database names. + */ + function MetaDatabases() { + global $ADODB_FETCH_MODE; + + if ($this->metaDatabasesSQL) { + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + if ($this->fetchMode !== false) { + $savem = $this->SetFetchMode(false); + } + + $arr = $this->GetCol($this->metaDatabasesSQL); + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + return $arr; + } + + return false; + } + + /** + * List procedures or functions in an array. + * @param procedureNamePattern a procedure name pattern; must match the procedure name as it is stored in the database + * @param catalog a catalog name; must match the catalog name as it is stored in the database; + * @param schemaPattern a schema name pattern; + * + * @return array of procedures on current database. + * + * Array( + * [name_of_procedure] => Array( + * [type] => PROCEDURE or FUNCTION + * [catalog] => Catalog_name + * [schema] => Schema_name + * [remarks] => explanatory comment on the procedure + * ) + * ) + */ + function MetaProcedures($procedureNamePattern = null, $catalog = null, $schemaPattern = null) { + return false; + } + + + /** + * @param ttype can either be 'VIEW' or 'TABLE' or false. + * If false, both views and tables are returned. + * "VIEW" returns only views + * "TABLE" returns only tables + * @param showSchema returns the schema/user with the table name, eg. USER.TABLE + * @param mask is the input mask - only supported by oci8 and postgresql + * + * @return array of tables for current database. + */ + function MetaTables($ttype=false,$showSchema=false,$mask=false) { + global $ADODB_FETCH_MODE; + + if ($mask) { + return false; + } + if ($this->metaTablesSQL) { + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + if ($this->fetchMode !== false) { + $savem = $this->SetFetchMode(false); + } + + $rs = $this->Execute($this->metaTablesSQL); + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + if ($rs === false) { + return false; + } + $arr = $rs->GetArray(); + $arr2 = array(); + + if ($hast = ($ttype && isset($arr[0][1]))) { + $showt = strncmp($ttype,'T',1); + } + + for ($i=0; $i < sizeof($arr); $i++) { + if ($hast) { + if ($showt == 0) { + if (strncmp($arr[$i][1],'T',1) == 0) { + $arr2[] = trim($arr[$i][0]); + } + } else { + if (strncmp($arr[$i][1],'V',1) == 0) { + $arr2[] = trim($arr[$i][0]); + } + } + } else + $arr2[] = trim($arr[$i][0]); + } + $rs->Close(); + return $arr2; + } + return false; + } + + + function _findschema(&$table,&$schema) { + if (!$schema && ($at = strpos($table,'.')) !== false) { + $schema = substr($table,0,$at); + $table = substr($table,$at+1); + } + } + + /** + * List columns in a database as an array of ADOFieldObjects. + * See top of file for definition of object. + * + * @param $table table name to query + * @param $normalize makes table name case-insensitive (required by some databases) + * @schema is optional database schema to use - not supported by all databases. + * + * @return array of ADOFieldObjects for current table. + */ + function MetaColumns($table,$normalize=true) { + global $ADODB_FETCH_MODE; + + if (!empty($this->metaColumnsSQL)) { + $schema = false; + $this->_findschema($table,$schema); + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== false) { + $savem = $this->SetFetchMode(false); + } + $rs = $this->Execute(sprintf($this->metaColumnsSQL,($normalize)?strtoupper($table):$table)); + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + if ($rs === false || $rs->EOF) { + return false; + } + + $retarr = array(); + while (!$rs->EOF) { //print_r($rs->fields); + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; + $fld->type = $rs->fields[1]; + if (isset($rs->fields[3]) && $rs->fields[3]) { + if ($rs->fields[3]>0) { + $fld->max_length = $rs->fields[3]; + } + $fld->scale = $rs->fields[4]; + if ($fld->scale>0) { + $fld->max_length += 1; + } + } else { + $fld->max_length = $rs->fields[2]; + } + + if ($ADODB_FETCH_MODE == ADODB_FETCH_NUM) { + $retarr[] = $fld; + } else { + $retarr[strtoupper($fld->name)] = $fld; + } + $rs->MoveNext(); + } + $rs->Close(); + return $retarr; + } + return false; + } + + /** + * List indexes on a table as an array. + * @param table table name to query + * @param primary true to only show primary keys. Not actually used for most databases + * + * @return array of indexes on current table. Each element represents an index, and is itself an associative array. + * + * Array( + * [name_of_index] => Array( + * [unique] => true or false + * [columns] => Array( + * [0] => firstname + * [1] => lastname + * ) + * ) + * ) + */ + function MetaIndexes($table, $primary = false, $owner = false) { + return false; + } + + /** + * List columns names in a table as an array. + * @param table table name to query + * + * @return array of column names for current table. + */ + function MetaColumnNames($table, $numIndexes=false,$useattnum=false /* only for postgres */) { + $objarr = $this->MetaColumns($table); + if (!is_array($objarr)) { + return false; + } + $arr = array(); + if ($numIndexes) { + $i = 0; + if ($useattnum) { + foreach($objarr as $v) + $arr[$v->attnum] = $v->name; + + } else + foreach($objarr as $v) $arr[$i++] = $v->name; + } else + foreach($objarr as $v) $arr[strtoupper($v->name)] = $v->name; + + return $arr; + } + + /** + * Different SQL databases used different methods to combine strings together. + * This function provides a wrapper. + * + * param s variable number of string parameters + * + * Usage: $db->Concat($str1,$str2); + * + * @return concatenated string + */ + function Concat() { + $arr = func_get_args(); + return implode($this->concat_operator, $arr); + } + + + /** + * Converts a date "d" to a string that the database can understand. + * + * @param d a date in Unix date time format. + * + * @return date string in database date format + */ + function DBDate($d, $isfld=false) { + if (empty($d) && $d !== 0) { + return 'null'; + } + if ($isfld) { + return $d; + } + if (is_object($d)) { + return $d->format($this->fmtDate); + } + + if (is_string($d) && !is_numeric($d)) { + if ($d === 'null') { + return $d; + } + if (strncmp($d,"'",1) === 0) { + $d = _adodb_safedateq($d); + return $d; + } + if ($this->isoDates) { + return "'$d'"; + } + $d = ADOConnection::UnixDate($d); + } + + return adodb_date($this->fmtDate,$d); + } + + function BindDate($d) { + $d = $this->DBDate($d); + if (strncmp($d,"'",1)) { + return $d; + } + + return substr($d,1,strlen($d)-2); + } + + function BindTimeStamp($d) { + $d = $this->DBTimeStamp($d); + if (strncmp($d,"'",1)) { + return $d; + } + + return substr($d,1,strlen($d)-2); + } + + + /** + * Converts a timestamp "ts" to a string that the database can understand. + * + * @param ts a timestamp in Unix date time format. + * + * @return timestamp string in database timestamp format + */ + function DBTimeStamp($ts,$isfld=false) { + if (empty($ts) && $ts !== 0) { + return 'null'; + } + if ($isfld) { + return $ts; + } + if (is_object($ts)) { + return $ts->format($this->fmtTimeStamp); + } + + # strlen(14) allows YYYYMMDDHHMMSS format + if (!is_string($ts) || (is_numeric($ts) && strlen($ts)<14)) { + return adodb_date($this->fmtTimeStamp,$ts); + } + + if ($ts === 'null') { + return $ts; + } + if ($this->isoDates && strlen($ts) !== 14) { + $ts = _adodb_safedate($ts); + return "'$ts'"; + } + $ts = ADOConnection::UnixTimeStamp($ts); + return adodb_date($this->fmtTimeStamp,$ts); + } + + /** + * Also in ADORecordSet. + * @param $v is a date string in YYYY-MM-DD format + * + * @return date in unix timestamp format, or 0 if before TIMESTAMP_FIRST_YEAR, or false if invalid date format + */ + static function UnixDate($v) { + if (is_object($v)) { + // odbtp support + //( [year] => 2004 [month] => 9 [day] => 4 [hour] => 12 [minute] => 44 [second] => 8 [fraction] => 0 ) + return adodb_mktime($v->hour,$v->minute,$v->second,$v->month,$v->day, $v->year); + } + + if (is_numeric($v) && strlen($v) !== 8) { + return $v; + } + if (!preg_match( "|^([0-9]{4})[-/\.]?([0-9]{1,2})[-/\.]?([0-9]{1,2})|", $v, $rr)) { + return false; + } + + if ($rr[1] <= TIMESTAMP_FIRST_YEAR) { + return 0; + } + + // h-m-s-MM-DD-YY + return @adodb_mktime(0,0,0,$rr[2],$rr[3],$rr[1]); + } + + + /** + * Also in ADORecordSet. + * @param $v is a timestamp string in YYYY-MM-DD HH-NN-SS format + * + * @return date in unix timestamp format, or 0 if before TIMESTAMP_FIRST_YEAR, or false if invalid date format + */ + static function UnixTimeStamp($v) { + if (is_object($v)) { + // odbtp support + //( [year] => 2004 [month] => 9 [day] => 4 [hour] => 12 [minute] => 44 [second] => 8 [fraction] => 0 ) + return adodb_mktime($v->hour,$v->minute,$v->second,$v->month,$v->day, $v->year); + } + + if (!preg_match( + "|^([0-9]{4})[-/\.]?([0-9]{1,2})[-/\.]?([0-9]{1,2})[ ,-]*(([0-9]{1,2}):?([0-9]{1,2}):?([0-9\.]{1,4}))?|", + ($v), $rr)) return false; + + if ($rr[1] <= TIMESTAMP_FIRST_YEAR && $rr[2]<= 1) { + return 0; + } + + // h-m-s-MM-DD-YY + if (!isset($rr[5])) { + return adodb_mktime(0,0,0,$rr[2],$rr[3],$rr[1]); + } + return @adodb_mktime($rr[5],$rr[6],$rr[7],$rr[2],$rr[3],$rr[1]); + } + + /** + * Also in ADORecordSet. + * + * Format database date based on user defined format. + * + * @param v is the character date in YYYY-MM-DD format, returned by database + * @param fmt is the format to apply to it, using date() + * + * @return a date formated as user desires + */ + function UserDate($v,$fmt='Y-m-d',$gmt=false) { + $tt = $this->UnixDate($v); + + // $tt == -1 if pre TIMESTAMP_FIRST_YEAR + if (($tt === false || $tt == -1) && $v != false) { + return $v; + } else if ($tt == 0) { + return $this->emptyDate; + } else if ($tt == -1) { + // pre-TIMESTAMP_FIRST_YEAR + } + + return ($gmt) ? adodb_gmdate($fmt,$tt) : adodb_date($fmt,$tt); + + } + + /** + * + * @param v is the character timestamp in YYYY-MM-DD hh:mm:ss format + * @param fmt is the format to apply to it, using date() + * + * @return a timestamp formated as user desires + */ + function UserTimeStamp($v,$fmt='Y-m-d H:i:s',$gmt=false) { + if (!isset($v)) { + return $this->emptyTimeStamp; + } + # strlen(14) allows YYYYMMDDHHMMSS format + if (is_numeric($v) && strlen($v)<14) { + return ($gmt) ? adodb_gmdate($fmt,$v) : adodb_date($fmt,$v); + } + $tt = $this->UnixTimeStamp($v); + // $tt == -1 if pre TIMESTAMP_FIRST_YEAR + if (($tt === false || $tt == -1) && $v != false) { + return $v; + } + if ($tt == 0) { + return $this->emptyTimeStamp; + } + return ($gmt) ? adodb_gmdate($fmt,$tt) : adodb_date($fmt,$tt); + } + + function escape($s,$magic_quotes=false) { + return $this->addq($s,$magic_quotes); + } + + /** + * Quotes a string, without prefixing nor appending quotes. + */ + function addq($s,$magic_quotes=false) { + if (!$magic_quotes) { + if ($this->replaceQuote[0] == '\\') { + // only since php 4.0.5 + $s = adodb_str_replace(array('\\',"\0"),array('\\\\',"\\\0"),$s); + //$s = str_replace("\0","\\\0", str_replace('\\','\\\\',$s)); + } + return str_replace("'",$this->replaceQuote,$s); + } + + // undo magic quotes for " + $s = str_replace('\\"','"',$s); + + if ($this->replaceQuote == "\\'" || ini_get('magic_quotes_sybase')) { + // ' already quoted, no need to change anything + return $s; + } else { + // change \' to '' for sybase/mssql + $s = str_replace('\\\\','\\',$s); + return str_replace("\\'",$this->replaceQuote,$s); + } + } + + /** + * Correctly quotes a string so that all strings are escaped. We prefix and append + * to the string single-quotes. + * An example is $db->qstr("Don't bother",magic_quotes_runtime()); + * + * @param s the string to quote + * @param [magic_quotes] if $s is GET/POST var, set to get_magic_quotes_gpc(). + * This undoes the stupidity of magic quotes for GPC. + * + * @return quoted string to be sent back to database + */ + function qstr($s,$magic_quotes=false) { + if (!$magic_quotes) { + if ($this->replaceQuote[0] == '\\'){ + // only since php 4.0.5 + $s = adodb_str_replace(array('\\',"\0"),array('\\\\',"\\\0"),$s); + //$s = str_replace("\0","\\\0", str_replace('\\','\\\\',$s)); + } + return "'".str_replace("'",$this->replaceQuote,$s)."'"; + } + + // undo magic quotes for " + $s = str_replace('\\"','"',$s); + + if ($this->replaceQuote == "\\'" || ini_get('magic_quotes_sybase')) { + // ' already quoted, no need to change anything + return "'$s'"; + } else { + // change \' to '' for sybase/mssql + $s = str_replace('\\\\','\\',$s); + return "'".str_replace("\\'",$this->replaceQuote,$s)."'"; + } + } + + + /** + * Will select the supplied $page number from a recordset, given that it is paginated in pages of + * $nrows rows per page. It also saves two boolean values saying if the given page is the first + * and/or last one of the recordset. Added by Iván Oliva to provide recordset pagination. + * + * See docs-adodb.htm#ex8 for an example of usage. + * + * @param sql + * @param nrows is the number of rows per page to get + * @param page is the page number to get (1-based) + * @param [inputarr] array of bind variables + * @param [secs2cache] is a private parameter only used by jlim + * @return the recordset ($rs->databaseType == 'array') + * + * NOTE: phpLens uses a different algorithm and does not use PageExecute(). + * + */ + function PageExecute($sql, $nrows, $page, $inputarr=false, $secs2cache=0) { + global $ADODB_INCLUDED_LIB; + if (empty($ADODB_INCLUDED_LIB)) { + include(ADODB_DIR.'/adodb-lib.inc.php'); + } + if ($this->pageExecuteCountRows) { + $rs = _adodb_pageexecute_all_rows($this, $sql, $nrows, $page, $inputarr, $secs2cache); + } else { + $rs = _adodb_pageexecute_no_last_page($this, $sql, $nrows, $page, $inputarr, $secs2cache); + } + return $rs; + } + + + /** + * Will select the supplied $page number from a recordset, given that it is paginated in pages of + * $nrows rows per page. It also saves two boolean values saying if the given page is the first + * and/or last one of the recordset. Added by Iván Oliva to provide recordset pagination. + * + * @param secs2cache seconds to cache data, set to 0 to force query + * @param sql + * @param nrows is the number of rows per page to get + * @param page is the page number to get (1-based) + * @param [inputarr] array of bind variables + * @return the recordset ($rs->databaseType == 'array') + */ + function CachePageExecute($secs2cache, $sql, $nrows, $page,$inputarr=false) { + /*switch($this->dataProvider) { + case 'postgres': + case 'mysql': + break; + default: $secs2cache = 0; break; + }*/ + $rs = $this->PageExecute($sql,$nrows,$page,$inputarr,$secs2cache); + return $rs; + } + +} // end class ADOConnection + + + + //============================================================================================== + // CLASS ADOFetchObj + //============================================================================================== + + /** + * Internal placeholder for record objects. Used by ADORecordSet->FetchObj(). + */ + class ADOFetchObj { + }; + + //============================================================================================== + // CLASS ADORecordSet_empty + //============================================================================================== + + class ADODB_Iterator_empty implements Iterator { + + private $rs; + + function __construct($rs) { + $this->rs = $rs; + } + + function rewind() {} + + function valid() { + return !$this->rs->EOF; + } + + function key() { + return false; + } + + function current() { + return false; + } + + function next() {} + + function __call($func, $params) { + return call_user_func_array(array($this->rs, $func), $params); + } + + function hasMore() { + return false; + } + + } + + + /** + * Lightweight recordset when there are no records to be returned + */ + class ADORecordSet_empty implements IteratorAggregate + { + var $dataProvider = 'empty'; + var $databaseType = false; + var $EOF = true; + var $_numOfRows = 0; + var $fields = false; + var $connection = false; + + function RowCount() { + return 0; + } + + function RecordCount() { + return 0; + } + + function PO_RecordCount() { + return 0; + } + + function Close() { + return true; + } + + function FetchRow() { + return false; + } + + function FieldCount() { + return 0; + } + + function Init() {} + + function getIterator() { + return new ADODB_Iterator_empty($this); + } + + function GetAssoc() { + return array(); + } + + function GetArray() { + return array(); + } + + function GetAll() { + return array(); + } + + function GetArrayLimit() { + return array(); + } + + function GetRows() { + return array(); + } + + function GetRowAssoc() { + return array(); + } + + function MaxRecordCount() { + return 0; + } + + function NumRows() { + return 0; + } + + function NumCols() { + return 0; + } + } + + //============================================================================================== + // DATE AND TIME FUNCTIONS + //============================================================================================== + if (!defined('ADODB_DATE_VERSION')) { + include(ADODB_DIR.'/adodb-time.inc.php'); + } + + //============================================================================================== + // CLASS ADORecordSet + //============================================================================================== + + class ADODB_Iterator implements Iterator { + + private $rs; + + function __construct($rs) { + $this->rs = $rs; + } + + function rewind() { + $this->rs->MoveFirst(); + } + + function valid() { + return !$this->rs->EOF; + } + + function key() { + return $this->rs->_currentRow; + } + + function current() { + return $this->rs->fields; + } + + function next() { + $this->rs->MoveNext(); + } + + function __call($func, $params) { + return call_user_func_array(array($this->rs, $func), $params); + } + + function hasMore() { + return !$this->rs->EOF; + } + + } + + + /** + * RecordSet class that represents the dataset returned by the database. + * To keep memory overhead low, this class holds only the current row in memory. + * No prefetching of data is done, so the RecordCount() can return -1 ( which + * means recordcount not known). + */ + class ADORecordSet implements IteratorAggregate { + + /** + * public variables + */ + var $dataProvider = "native"; + var $fields = false; /// holds the current row data + var $blobSize = 100; /// any varchar/char field this size or greater is treated as a blob + /// in other words, we use a text area for editing. + var $canSeek = false; /// indicates that seek is supported + var $sql; /// sql text + var $EOF = false; /// Indicates that the current record position is after the last record in a Recordset object. + + var $emptyTimeStamp = ' '; /// what to display when $time==0 + var $emptyDate = ' '; /// what to display when $time==0 + var $debug = false; + var $timeCreated=0; /// datetime in Unix format rs created -- for cached recordsets + + var $bind = false; /// used by Fields() to hold array - should be private? + var $fetchMode; /// default fetch mode + var $connection = false; /// the parent connection + + /** + * private variables + */ + var $_numOfRows = -1; /** number of rows, or -1 */ + var $_numOfFields = -1; /** number of fields in recordset */ + var $_queryID = -1; /** This variable keeps the result link identifier. */ + var $_currentRow = -1; /** This variable keeps the current row in the Recordset. */ + var $_closed = false; /** has recordset been closed */ + var $_inited = false; /** Init() should only be called once */ + var $_obj; /** Used by FetchObj */ + var $_names; /** Used by FetchObj */ + + var $_currentPage = -1; /** Added by Iván Oliva to implement recordset pagination */ + var $_atFirstPage = false; /** Added by Iván Oliva to implement recordset pagination */ + var $_atLastPage = false; /** Added by Iván Oliva to implement recordset pagination */ + var $_lastPageNo = -1; + var $_maxRecordCount = 0; + var $datetime = false; + + /** + * Constructor + * + * @param queryID this is the queryID returned by ADOConnection->_query() + * + */ + function __construct($queryID) { + $this->_queryID = $queryID; + } + + function __destruct() { + $this->Close(); + } + + function getIterator() { + return new ADODB_Iterator($this); + } + + /* this is experimental - i don't really know what to return... */ + function __toString() { + include_once(ADODB_DIR.'/toexport.inc.php'); + return _adodb_export($this,',',',',false,true); + } + + function Init() { + if ($this->_inited) { + return; + } + $this->_inited = true; + if ($this->_queryID) { + @$this->_initrs(); + } else { + $this->_numOfRows = 0; + $this->_numOfFields = 0; + } + if ($this->_numOfRows != 0 && $this->_numOfFields && $this->_currentRow == -1) { + $this->_currentRow = 0; + if ($this->EOF = ($this->_fetch() === false)) { + $this->_numOfRows = 0; // _numOfRows could be -1 + } + } else { + $this->EOF = true; + } + } + + + /** + * Generate a SELECT tag string from a recordset, and return the string. + * If the recordset has 2 cols, we treat the 1st col as the containing + * the text to display to the user, and 2nd col as the return value. Default + * strings are compared with the FIRST column. + * + * @param name name of SELECT tag + * @param [defstr] the value to hilite. Use an array for multiple hilites for listbox. + * @param [blank1stItem] true to leave the 1st item in list empty + * @param [multiple] true for listbox, false for popup + * @param [size] #rows to show for listbox. not used by popup + * @param [selectAttr] additional attributes to defined for SELECT tag. + * useful for holding javascript onChange='...' handlers. + & @param [compareFields0] when we have 2 cols in recordset, we compare the defstr with + * column 0 (1st col) if this is true. This is not documented. + * + * @return HTML + * + * changes by glen.davies@cce.ac.nz to support multiple hilited items + */ + function GetMenu($name,$defstr='',$blank1stItem=true,$multiple=false, + $size=0, $selectAttr='',$compareFields0=true) + { + global $ADODB_INCLUDED_LIB; + if (empty($ADODB_INCLUDED_LIB)) { + include(ADODB_DIR.'/adodb-lib.inc.php'); + } + return _adodb_getmenu($this, $name,$defstr,$blank1stItem,$multiple, + $size, $selectAttr,$compareFields0); + } + + + + /** + * Generate a SELECT tag string from a recordset, and return the string. + * If the recordset has 2 cols, we treat the 1st col as the containing + * the text to display to the user, and 2nd col as the return value. Default + * strings are compared with the SECOND column. + * + */ + function GetMenu2($name,$defstr='',$blank1stItem=true,$multiple=false,$size=0, $selectAttr='') { + return $this->GetMenu($name,$defstr,$blank1stItem,$multiple, + $size, $selectAttr,false); + } + + /* + Grouped Menu + */ + function GetMenu3($name,$defstr='',$blank1stItem=true,$multiple=false, + $size=0, $selectAttr='') + { + global $ADODB_INCLUDED_LIB; + if (empty($ADODB_INCLUDED_LIB)) { + include(ADODB_DIR.'/adodb-lib.inc.php'); + } + return _adodb_getmenu_gp($this, $name,$defstr,$blank1stItem,$multiple, + $size, $selectAttr,false); + } + + /** + * return recordset as a 2-dimensional array. + * + * @param [nRows] is the number of rows to return. -1 means every row. + * + * @return an array indexed by the rows (0-based) from the recordset + */ + function GetArray($nRows = -1) { + global $ADODB_EXTENSION; if ($ADODB_EXTENSION) { + $results = adodb_getall($this,$nRows); + return $results; + } + $results = array(); + $cnt = 0; + while (!$this->EOF && $nRows != $cnt) { + $results[] = $this->fields; + $this->MoveNext(); + $cnt++; + } + return $results; + } + + function GetAll($nRows = -1) { + $arr = $this->GetArray($nRows); + return $arr; + } + + /* + * Some databases allow multiple recordsets to be returned. This function + * will return true if there is a next recordset, or false if no more. + */ + function NextRecordSet() { + return false; + } + + /** + * return recordset as a 2-dimensional array. + * Helper function for ADOConnection->SelectLimit() + * + * @param offset is the row to start calculations from (1-based) + * @param [nrows] is the number of rows to return + * + * @return an array indexed by the rows (0-based) from the recordset + */ + function GetArrayLimit($nrows,$offset=-1) { + if ($offset <= 0) { + $arr = $this->GetArray($nrows); + return $arr; + } + + $this->Move($offset); + + $results = array(); + $cnt = 0; + while (!$this->EOF && $nrows != $cnt) { + $results[$cnt++] = $this->fields; + $this->MoveNext(); + } + + return $results; + } + + + /** + * Synonym for GetArray() for compatibility with ADO. + * + * @param [nRows] is the number of rows to return. -1 means every row. + * + * @return an array indexed by the rows (0-based) from the recordset + */ + function GetRows($nRows = -1) { + $arr = $this->GetArray($nRows); + return $arr; + } + + /** + * return whole recordset as a 2-dimensional associative array if there are more than 2 columns. + * The first column is treated as the key and is not included in the array. + * If there is only 2 columns, it will return a 1 dimensional array of key-value pairs unless + * $force_array == true. + * + * @param [force_array] has only meaning if we have 2 data columns. If false, a 1 dimensional + * array is returned, otherwise a 2 dimensional array is returned. If this sounds confusing, + * read the source. + * + * @param [first2cols] means if there are more than 2 cols, ignore the remaining cols and + * instead of returning array[col0] => array(remaining cols), return array[col0] => col1 + * + * @return an associative array indexed by the first column of the array, + * or false if the data has less than 2 cols. + */ + function GetAssoc($force_array = false, $first2cols = false) { + global $ADODB_EXTENSION; + + $cols = $this->_numOfFields; + if ($cols < 2) { + return false; + } + + // Empty recordset + if (!$this->fields) { + return array(); + } + + // Determine whether the array is associative or 0-based numeric + $numIndex = array_keys($this->fields) == range(0, count($this->fields) - 1); + + $results = array(); + + if (!$first2cols && ($cols > 2 || $force_array)) { + if ($ADODB_EXTENSION) { + if ($numIndex) { + while (!$this->EOF) { + $results[trim($this->fields[0])] = array_slice($this->fields, 1); + adodb_movenext($this); + } + } else { + while (!$this->EOF) { + // Fix for array_slice re-numbering numeric associative keys + $keys = array_slice(array_keys($this->fields), 1); + $sliced_array = array(); + + foreach($keys as $key) { + $sliced_array[$key] = $this->fields[$key]; + } + + $results[trim(reset($this->fields))] = $sliced_array; + adodb_movenext($this); + } + } + } else { + if ($numIndex) { + while (!$this->EOF) { + $results[trim($this->fields[0])] = array_slice($this->fields, 1); + $this->MoveNext(); + } + } else { + while (!$this->EOF) { + // Fix for array_slice re-numbering numeric associative keys + $keys = array_slice(array_keys($this->fields), 1); + $sliced_array = array(); + + foreach($keys as $key) { + $sliced_array[$key] = $this->fields[$key]; + } + + $results[trim(reset($this->fields))] = $sliced_array; + $this->MoveNext(); + } + } + } + } else { + if ($ADODB_EXTENSION) { + // return scalar values + if ($numIndex) { + while (!$this->EOF) { + // some bug in mssql PHP 4.02 -- doesn't handle references properly so we FORCE creating a new string + $results[trim(($this->fields[0]))] = $this->fields[1]; + adodb_movenext($this); + } + } else { + while (!$this->EOF) { + // some bug in mssql PHP 4.02 -- doesn't handle references properly so we FORCE creating a new string + $v1 = trim(reset($this->fields)); + $v2 = ''.next($this->fields); + $results[$v1] = $v2; + adodb_movenext($this); + } + } + } else { + if ($numIndex) { + while (!$this->EOF) { + // some bug in mssql PHP 4.02 -- doesn't handle references properly so we FORCE creating a new string + $results[trim(($this->fields[0]))] = $this->fields[1]; + $this->MoveNext(); + } + } else { + while (!$this->EOF) { + // some bug in mssql PHP 4.02 -- doesn't handle references properly so we FORCE creating a new string + $v1 = trim(reset($this->fields)); + $v2 = ''.next($this->fields); + $results[$v1] = $v2; + $this->MoveNext(); + } + } + } + } + + $ref = $results; # workaround accelerator incompat with PHP 4.4 :( + return $ref; + } + + + /** + * + * @param v is the character timestamp in YYYY-MM-DD hh:mm:ss format + * @param fmt is the format to apply to it, using date() + * + * @return a timestamp formated as user desires + */ + function UserTimeStamp($v,$fmt='Y-m-d H:i:s') { + if (is_numeric($v) && strlen($v)<14) { + return adodb_date($fmt,$v); + } + $tt = $this->UnixTimeStamp($v); + // $tt == -1 if pre TIMESTAMP_FIRST_YEAR + if (($tt === false || $tt == -1) && $v != false) { + return $v; + } + if ($tt === 0) { + return $this->emptyTimeStamp; + } + return adodb_date($fmt,$tt); + } + + + /** + * @param v is the character date in YYYY-MM-DD format, returned by database + * @param fmt is the format to apply to it, using date() + * + * @return a date formated as user desires + */ + function UserDate($v,$fmt='Y-m-d') { + $tt = $this->UnixDate($v); + // $tt == -1 if pre TIMESTAMP_FIRST_YEAR + if (($tt === false || $tt == -1) && $v != false) { + return $v; + } else if ($tt == 0) { + return $this->emptyDate; + } else if ($tt == -1) { + // pre-TIMESTAMP_FIRST_YEAR + } + return adodb_date($fmt,$tt); + } + + + /** + * @param $v is a date string in YYYY-MM-DD format + * + * @return date in unix timestamp format, or 0 if before TIMESTAMP_FIRST_YEAR, or false if invalid date format + */ + static function UnixDate($v) { + return ADOConnection::UnixDate($v); + } + + + /** + * @param $v is a timestamp string in YYYY-MM-DD HH-NN-SS format + * + * @return date in unix timestamp format, or 0 if before TIMESTAMP_FIRST_YEAR, or false if invalid date format + */ + static function UnixTimeStamp($v) { + return ADOConnection::UnixTimeStamp($v); + } + + + /** + * PEAR DB Compat - do not use internally + */ + function Free() { + return $this->Close(); + } + + + /** + * PEAR DB compat, number of rows + */ + function NumRows() { + return $this->_numOfRows; + } + + + /** + * PEAR DB compat, number of cols + */ + function NumCols() { + return $this->_numOfFields; + } + + /** + * Fetch a row, returning false if no more rows. + * This is PEAR DB compat mode. + * + * @return false or array containing the current record + */ + function FetchRow() { + if ($this->EOF) { + return false; + } + $arr = $this->fields; + $this->_currentRow++; + if (!$this->_fetch()) { + $this->EOF = true; + } + return $arr; + } + + + /** + * Fetch a row, returning PEAR_Error if no more rows. + * This is PEAR DB compat mode. + * + * @return DB_OK or error object + */ + function FetchInto(&$arr) { + if ($this->EOF) { + return (defined('PEAR_ERROR_RETURN')) ? new PEAR_Error('EOF',-1): false; + } + $arr = $this->fields; + $this->MoveNext(); + return 1; // DB_OK + } + + + /** + * Move to the first row in the recordset. Many databases do NOT support this. + * + * @return true or false + */ + function MoveFirst() { + if ($this->_currentRow == 0) { + return true; + } + return $this->Move(0); + } + + + /** + * Move to the last row in the recordset. + * + * @return true or false + */ + function MoveLast() { + if ($this->_numOfRows >= 0) { + return $this->Move($this->_numOfRows-1); + } + if ($this->EOF) { + return false; + } + while (!$this->EOF) { + $f = $this->fields; + $this->MoveNext(); + } + $this->fields = $f; + $this->EOF = false; + return true; + } + + + /** + * Move to next record in the recordset. + * + * @return true if there still rows available, or false if there are no more rows (EOF). + */ + function MoveNext() { + if (!$this->EOF) { + $this->_currentRow++; + if ($this->_fetch()) { + return true; + } + } + $this->EOF = true; + /* -- tested error handling when scrolling cursor -- seems useless. + $conn = $this->connection; + if ($conn && $conn->raiseErrorFn && ($errno = $conn->ErrorNo())) { + $fn = $conn->raiseErrorFn; + $fn($conn->databaseType,'MOVENEXT',$errno,$conn->ErrorMsg().' ('.$this->sql.')',$conn->host,$conn->database); + } + */ + return false; + } + + + /** + * Random access to a specific row in the recordset. Some databases do not support + * access to previous rows in the databases (no scrolling backwards). + * + * @param rowNumber is the row to move to (0-based) + * + * @return true if there still rows available, or false if there are no more rows (EOF). + */ + function Move($rowNumber = 0) { + $this->EOF = false; + if ($rowNumber == $this->_currentRow) { + return true; + } + if ($rowNumber >= $this->_numOfRows) { + if ($this->_numOfRows != -1) { + $rowNumber = $this->_numOfRows-2; + } + } + + if ($rowNumber < 0) { + $this->EOF = true; + return false; + } + + if ($this->canSeek) { + if ($this->_seek($rowNumber)) { + $this->_currentRow = $rowNumber; + if ($this->_fetch()) { + return true; + } + } else { + $this->EOF = true; + return false; + } + } else { + if ($rowNumber < $this->_currentRow) { + return false; + } + global $ADODB_EXTENSION; + if ($ADODB_EXTENSION) { + while (!$this->EOF && $this->_currentRow < $rowNumber) { + adodb_movenext($this); + } + } else { + while (! $this->EOF && $this->_currentRow < $rowNumber) { + $this->_currentRow++; + + if (!$this->_fetch()) { + $this->EOF = true; + } + } + } + return !($this->EOF); + } + + $this->fields = false; + $this->EOF = true; + return false; + } + + + /** + * Get the value of a field in the current row by column name. + * Will not work if ADODB_FETCH_MODE is set to ADODB_FETCH_NUM. + * + * @param colname is the field to access + * + * @return the value of $colname column + */ + function Fields($colname) { + return $this->fields[$colname]; + } + + /** + * Defines the function to use for table fields case conversion + * depending on ADODB_ASSOC_CASE + * @return string strtolower/strtoupper or false if no conversion needed + */ + protected function AssocCaseConvertFunction($case = ADODB_ASSOC_CASE) { + switch($case) { + case ADODB_ASSOC_CASE_UPPER: + return 'strtoupper'; + case ADODB_ASSOC_CASE_LOWER: + return 'strtolower'; + case ADODB_ASSOC_CASE_NATIVE: + default: + return false; + } + } + + /** + * Builds the bind array associating keys to recordset fields + * + * @param int $upper Case for the array keys, defaults to uppercase + * (see ADODB_ASSOC_CASE_xxx constants) + */ + function GetAssocKeys($upper = ADODB_ASSOC_CASE) { + if ($this->bind) { + return; + } + $this->bind = array(); + + // Define case conversion function for ASSOC fetch mode + $fn_change_case = $this->AssocCaseConvertFunction($upper); + + // Build the bind array + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + + // Set the array's key + if(is_numeric($o->name)) { + // Just use the field ID + $key = $i; + } + elseif( $fn_change_case ) { + // Convert the key's case + $key = $fn_change_case($o->name); + } + else { + $key = $o->name; + } + + $this->bind[$key] = $i; + } + } + + /** + * Use associative array to get fields array for databases that do not support + * associative arrays. Submitted by Paolo S. Asioli paolo.asioli#libero.it + * + * @param int $upper Case for the array keys, defaults to uppercase + * (see ADODB_ASSOC_CASE_xxx constants) + */ + function GetRowAssoc($upper = ADODB_ASSOC_CASE) { + $record = array(); + $this->GetAssocKeys($upper); + + foreach($this->bind as $k => $v) { + if( array_key_exists( $v, $this->fields ) ) { + $record[$k] = $this->fields[$v]; + } elseif( array_key_exists( $k, $this->fields ) ) { + $record[$k] = $this->fields[$k]; + } else { + # This should not happen... trigger error ? + $record[$k] = null; + } + } + return $record; + } + + /** + * Clean up recordset + * + * @return true or false + */ + function Close() { + // free connection object - this seems to globally free the object + // and not merely the reference, so don't do this... + // $this->connection = false; + if (!$this->_closed) { + $this->_closed = true; + return $this->_close(); + } else + return true; + } + + /** + * synonyms RecordCount and RowCount + * + * @return the number of rows or -1 if this is not supported + */ + function RecordCount() { + return $this->_numOfRows; + } + + + /* + * If we are using PageExecute(), this will return the maximum possible rows + * that can be returned when paging a recordset. + */ + function MaxRecordCount() { + return ($this->_maxRecordCount) ? $this->_maxRecordCount : $this->RecordCount(); + } + + /** + * synonyms RecordCount and RowCount + * + * @return the number of rows or -1 if this is not supported + */ + function RowCount() { + return $this->_numOfRows; + } + + + /** + * Portable RecordCount. Pablo Roca + * + * @return the number of records from a previous SELECT. All databases support this. + * + * But aware possible problems in multiuser environments. For better speed the table + * must be indexed by the condition. Heavy test this before deploying. + */ + function PO_RecordCount($table="", $condition="") { + + $lnumrows = $this->_numOfRows; + // the database doesn't support native recordcount, so we do a workaround + if ($lnumrows == -1 && $this->connection) { + IF ($table) { + if ($condition) { + $condition = " WHERE " . $condition; + } + $resultrows = $this->connection->Execute("SELECT COUNT(*) FROM $table $condition"); + if ($resultrows) { + $lnumrows = reset($resultrows->fields); + } + } + } + return $lnumrows; + } + + + /** + * @return the current row in the recordset. If at EOF, will return the last row. 0-based. + */ + function CurrentRow() { + return $this->_currentRow; + } + + /** + * synonym for CurrentRow -- for ADO compat + * + * @return the current row in the recordset. If at EOF, will return the last row. 0-based. + */ + function AbsolutePosition() { + return $this->_currentRow; + } + + /** + * @return the number of columns in the recordset. Some databases will set this to 0 + * if no records are returned, others will return the number of columns in the query. + */ + function FieldCount() { + return $this->_numOfFields; + } + + + /** + * Get the ADOFieldObject of a specific column. + * + * @param fieldoffset is the column position to access(0-based). + * + * @return the ADOFieldObject for that column, or false. + */ + function FetchField($fieldoffset = -1) { + // must be defined by child class + + return false; + } + + /** + * Get the ADOFieldObjects of all columns in an array. + * + */ + function FieldTypesArray() { + $arr = array(); + for ($i=0, $max=$this->_numOfFields; $i < $max; $i++) + $arr[] = $this->FetchField($i); + return $arr; + } + + /** + * Return the fields array of the current row as an object for convenience. + * The default case is lowercase field names. + * + * @return the object with the properties set to the fields of the current row + */ + function FetchObj() { + $o = $this->FetchObject(false); + return $o; + } + + /** + * Return the fields array of the current row as an object for convenience. + * The default case is uppercase. + * + * @param $isupper to set the object property names to uppercase + * + * @return the object with the properties set to the fields of the current row + */ + function FetchObject($isupper=true) { + if (empty($this->_obj)) { + $this->_obj = new ADOFetchObj(); + $this->_names = array(); + for ($i=0; $i <$this->_numOfFields; $i++) { + $f = $this->FetchField($i); + $this->_names[] = $f->name; + } + } + $i = 0; + if (PHP_VERSION >= 5) { + $o = clone($this->_obj); + } else { + $o = $this->_obj; + } + + for ($i=0; $i <$this->_numOfFields; $i++) { + $name = $this->_names[$i]; + if ($isupper) { + $n = strtoupper($name); + } else { + $n = $name; + } + + $o->$n = $this->Fields($name); + } + return $o; + } + + /** + * Return the fields array of the current row as an object for convenience. + * The default is lower-case field names. + * + * @return the object with the properties set to the fields of the current row, + * or false if EOF + * + * Fixed bug reported by tim@orotech.net + */ + function FetchNextObj() { + $o = $this->FetchNextObject(false); + return $o; + } + + + /** + * Return the fields array of the current row as an object for convenience. + * The default is upper case field names. + * + * @param $isupper to set the object property names to uppercase + * + * @return the object with the properties set to the fields of the current row, + * or false if EOF + * + * Fixed bug reported by tim@orotech.net + */ + function FetchNextObject($isupper=true) { + $o = false; + if ($this->_numOfRows != 0 && !$this->EOF) { + $o = $this->FetchObject($isupper); + $this->_currentRow++; + if ($this->_fetch()) { + return $o; + } + } + $this->EOF = true; + return $o; + } + + /** + * Get the metatype of the column. This is used for formatting. This is because + * many databases use different names for the same type, so we transform the original + * type to our standardised version which uses 1 character codes: + * + * @param t is the type passed in. Normally is ADOFieldObject->type. + * @param len is the maximum length of that field. This is because we treat character + * fields bigger than a certain size as a 'B' (blob). + * @param fieldobj is the field object returned by the database driver. Can hold + * additional info (eg. primary_key for mysql). + * + * @return the general type of the data: + * C for character < 250 chars + * X for teXt (>= 250 chars) + * B for Binary + * N for numeric or floating point + * D for date + * T for timestamp + * L for logical/Boolean + * I for integer + * R for autoincrement counter/integer + * + * + */ + function MetaType($t,$len=-1,$fieldobj=false) { + if (is_object($t)) { + $fieldobj = $t; + $t = $fieldobj->type; + $len = $fieldobj->max_length; + } + + // changed in 2.32 to hashing instead of switch stmt for speed... + static $typeMap = array( + 'VARCHAR' => 'C', + 'VARCHAR2' => 'C', + 'CHAR' => 'C', + 'C' => 'C', + 'STRING' => 'C', + 'NCHAR' => 'C', + 'NVARCHAR' => 'C', + 'VARYING' => 'C', + 'BPCHAR' => 'C', + 'CHARACTER' => 'C', + 'INTERVAL' => 'C', # Postgres + 'MACADDR' => 'C', # postgres + 'VAR_STRING' => 'C', # mysql + ## + 'LONGCHAR' => 'X', + 'TEXT' => 'X', + 'NTEXT' => 'X', + 'M' => 'X', + 'X' => 'X', + 'CLOB' => 'X', + 'NCLOB' => 'X', + 'LVARCHAR' => 'X', + ## + 'BLOB' => 'B', + 'IMAGE' => 'B', + 'BINARY' => 'B', + 'VARBINARY' => 'B', + 'LONGBINARY' => 'B', + 'B' => 'B', + ## + 'YEAR' => 'D', // mysql + 'DATE' => 'D', + 'D' => 'D', + ## + 'UNIQUEIDENTIFIER' => 'C', # MS SQL Server + ## + 'SMALLDATETIME' => 'T', + 'TIME' => 'T', + 'TIMESTAMP' => 'T', + 'DATETIME' => 'T', + 'DATETIME2' => 'T', + 'TIMESTAMPTZ' => 'T', + 'T' => 'T', + 'TIMESTAMP WITHOUT TIME ZONE' => 'T', // postgresql + ## + 'BOOL' => 'L', + 'BOOLEAN' => 'L', + 'BIT' => 'L', + 'L' => 'L', + ## + 'COUNTER' => 'R', + 'R' => 'R', + 'SERIAL' => 'R', // ifx + 'INT IDENTITY' => 'R', + ## + 'INT' => 'I', + 'INT2' => 'I', + 'INT4' => 'I', + 'INT8' => 'I', + 'INTEGER' => 'I', + 'INTEGER UNSIGNED' => 'I', + 'SHORT' => 'I', + 'TINYINT' => 'I', + 'SMALLINT' => 'I', + 'I' => 'I', + ## + 'LONG' => 'N', // interbase is numeric, oci8 is blob + 'BIGINT' => 'N', // this is bigger than PHP 32-bit integers + 'DECIMAL' => 'N', + 'DEC' => 'N', + 'REAL' => 'N', + 'DOUBLE' => 'N', + 'DOUBLE PRECISION' => 'N', + 'SMALLFLOAT' => 'N', + 'FLOAT' => 'N', + 'NUMBER' => 'N', + 'NUM' => 'N', + 'NUMERIC' => 'N', + 'MONEY' => 'N', + + ## informix 9.2 + 'SQLINT' => 'I', + 'SQLSERIAL' => 'I', + 'SQLSMINT' => 'I', + 'SQLSMFLOAT' => 'N', + 'SQLFLOAT' => 'N', + 'SQLMONEY' => 'N', + 'SQLDECIMAL' => 'N', + 'SQLDATE' => 'D', + 'SQLVCHAR' => 'C', + 'SQLCHAR' => 'C', + 'SQLDTIME' => 'T', + 'SQLINTERVAL' => 'N', + 'SQLBYTES' => 'B', + 'SQLTEXT' => 'X', + ## informix 10 + "SQLINT8" => 'I8', + "SQLSERIAL8" => 'I8', + "SQLNCHAR" => 'C', + "SQLNVCHAR" => 'C', + "SQLLVARCHAR" => 'X', + "SQLBOOL" => 'L' + ); + + $tmap = false; + $t = strtoupper($t); + $tmap = (isset($typeMap[$t])) ? $typeMap[$t] : 'N'; + switch ($tmap) { + case 'C': + // is the char field is too long, return as text field... + if ($this->blobSize >= 0) { + if ($len > $this->blobSize) { + return 'X'; + } + } else if ($len > 250) { + return 'X'; + } + return 'C'; + + case 'I': + if (!empty($fieldobj->primary_key)) { + return 'R'; + } + return 'I'; + + case false: + return 'N'; + + case 'B': + if (isset($fieldobj->binary)) { + return ($fieldobj->binary) ? 'B' : 'X'; + } + return 'B'; + + case 'D': + if (!empty($this->connection) && !empty($this->connection->datetime)) { + return 'T'; + } + return 'D'; + + default: + if ($t == 'LONG' && $this->dataProvider == 'oci8') { + return 'B'; + } + return $tmap; + } + } + + /** + * Convert case of field names associative array, if needed + * @return void + */ + protected function _updatefields() + { + if( empty($this->fields)) { + return; + } + + // Determine case conversion function + $fn_change_case = $this->AssocCaseConvertFunction(); + if(!$fn_change_case) { + // No conversion needed + return; + } + + $arr = array(); + + // Change the case + foreach($this->fields as $k => $v) { + if (!is_integer($k)) { + $k = $fn_change_case($k); + } + $arr[$k] = $v; + } + $this->fields = $arr; + } + + function _close() {} + + /** + * set/returns the current recordset page when paginating + */ + function AbsolutePage($page=-1) { + if ($page != -1) { + $this->_currentPage = $page; + } + return $this->_currentPage; + } + + /** + * set/returns the status of the atFirstPage flag when paginating + */ + function AtFirstPage($status=false) { + if ($status != false) { + $this->_atFirstPage = $status; + } + return $this->_atFirstPage; + } + + function LastPageNo($page = false) { + if ($page != false) { + $this->_lastPageNo = $page; + } + return $this->_lastPageNo; + } + + /** + * set/returns the status of the atLastPage flag when paginating + */ + function AtLastPage($status=false) { + if ($status != false) { + $this->_atLastPage = $status; + } + return $this->_atLastPage; + } + +} // end class ADORecordSet + + //============================================================================================== + // CLASS ADORecordSet_array + //============================================================================================== + + /** + * This class encapsulates the concept of a recordset created in memory + * as an array. This is useful for the creation of cached recordsets. + * + * Note that the constructor is different from the standard ADORecordSet + */ + class ADORecordSet_array extends ADORecordSet + { + var $databaseType = 'array'; + + var $_array; // holds the 2-dimensional data array + var $_types; // the array of types of each column (C B I L M) + var $_colnames; // names of each column in array + var $_skiprow1; // skip 1st row because it holds column names + var $_fieldobjects; // holds array of field objects + var $canSeek = true; + var $affectedrows = false; + var $insertid = false; + var $sql = ''; + var $compat = false; + + /** + * Constructor + */ + function __construct($fakeid=1) { + global $ADODB_FETCH_MODE,$ADODB_COMPAT_FETCH; + + // fetch() on EOF does not delete $this->fields + $this->compat = !empty($ADODB_COMPAT_FETCH); + parent::__construct($fakeid); // fake queryID + $this->fetchMode = $ADODB_FETCH_MODE; + } + + function _transpose($addfieldnames=true) { + global $ADODB_INCLUDED_LIB; + + if (empty($ADODB_INCLUDED_LIB)) { + include(ADODB_DIR.'/adodb-lib.inc.php'); + } + $hdr = true; + + $fobjs = $addfieldnames ? $this->_fieldobjects : false; + adodb_transpose($this->_array, $newarr, $hdr, $fobjs); + //adodb_pr($newarr); + + $this->_skiprow1 = false; + $this->_array = $newarr; + $this->_colnames = $hdr; + + adodb_probetypes($newarr,$this->_types); + + $this->_fieldobjects = array(); + + foreach($hdr as $k => $name) { + $f = new ADOFieldObject(); + $f->name = $name; + $f->type = $this->_types[$k]; + $f->max_length = -1; + $this->_fieldobjects[] = $f; + } + $this->fields = reset($this->_array); + + $this->_initrs(); + + } + + /** + * Setup the array. + * + * @param array is a 2-dimensional array holding the data. + * The first row should hold the column names + * unless paramter $colnames is used. + * @param typearr holds an array of types. These are the same types + * used in MetaTypes (C,B,L,I,N). + * @param [colnames] array of column names. If set, then the first row of + * $array should not hold the column names. + */ + function InitArray($array,$typearr,$colnames=false) { + $this->_array = $array; + $this->_types = $typearr; + if ($colnames) { + $this->_skiprow1 = false; + $this->_colnames = $colnames; + } else { + $this->_skiprow1 = true; + $this->_colnames = $array[0]; + } + $this->Init(); + } + /** + * Setup the Array and datatype file objects + * + * @param array is a 2-dimensional array holding the data. + * The first row should hold the column names + * unless paramter $colnames is used. + * @param fieldarr holds an array of ADOFieldObject's. + */ + function InitArrayFields(&$array,&$fieldarr) { + $this->_array = $array; + $this->_skiprow1= false; + if ($fieldarr) { + $this->_fieldobjects = $fieldarr; + } + $this->Init(); + } + + function GetArray($nRows=-1) { + if ($nRows == -1 && $this->_currentRow <= 0 && !$this->_skiprow1) { + return $this->_array; + } else { + $arr = ADORecordSet::GetArray($nRows); + return $arr; + } + } + + function _initrs() { + $this->_numOfRows = sizeof($this->_array); + if ($this->_skiprow1) { + $this->_numOfRows -= 1; + } + + $this->_numOfFields = (isset($this->_fieldobjects)) + ? sizeof($this->_fieldobjects) + : sizeof($this->_types); + } + + /* Use associative array to get fields array */ + function Fields($colname) { + $mode = isset($this->adodbFetchMode) ? $this->adodbFetchMode : $this->fetchMode; + + if ($mode & ADODB_FETCH_ASSOC) { + if (!isset($this->fields[$colname]) && !is_null($this->fields[$colname])) { + $colname = strtolower($colname); + } + return $this->fields[$colname]; + } + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + function FetchField($fieldOffset = -1) { + if (isset($this->_fieldobjects)) { + return $this->_fieldobjects[$fieldOffset]; + } + $o = new ADOFieldObject(); + $o->name = $this->_colnames[$fieldOffset]; + $o->type = $this->_types[$fieldOffset]; + $o->max_length = -1; // length not known + + return $o; + } + + function _seek($row) { + if (sizeof($this->_array) && 0 <= $row && $row < $this->_numOfRows) { + $this->_currentRow = $row; + if ($this->_skiprow1) { + $row += 1; + } + $this->fields = $this->_array[$row]; + return true; + } + return false; + } + + function MoveNext() { + if (!$this->EOF) { + $this->_currentRow++; + + $pos = $this->_currentRow; + + if ($this->_numOfRows <= $pos) { + if (!$this->compat) { + $this->fields = false; + } + } else { + if ($this->_skiprow1) { + $pos += 1; + } + $this->fields = $this->_array[$pos]; + return true; + } + $this->EOF = true; + } + + return false; + } + + function _fetch() { + $pos = $this->_currentRow; + + if ($this->_numOfRows <= $pos) { + if (!$this->compat) { + $this->fields = false; + } + return false; + } + if ($this->_skiprow1) { + $pos += 1; + } + $this->fields = $this->_array[$pos]; + return true; + } + + function _close() { + return true; + } + + } // ADORecordSet_array + + //============================================================================================== + // HELPER FUNCTIONS + //============================================================================================== + + /** + * Synonym for ADOLoadCode. Private function. Do not use. + * + * @deprecated + */ + function ADOLoadDB($dbType) { + return ADOLoadCode($dbType); + } + + /** + * Load the code for a specific database driver. Private function. Do not use. + */ + function ADOLoadCode($dbType) { + global $ADODB_LASTDB; + + if (!$dbType) { + return false; + } + $db = strtolower($dbType); + switch ($db) { + case 'ado': + if (PHP_VERSION >= 5) { + $db = 'ado5'; + } + $class = 'ado'; + break; + + case 'ifx': + case 'maxsql': + $class = $db = 'mysqlt'; + break; + + case 'pgsql': + case 'postgres': + $class = $db = 'postgres8'; + break; + + default: + $class = $db; break; + } + + $file = ADODB_DIR."/drivers/adodb-".$db.".inc.php"; + @include_once($file); + $ADODB_LASTDB = $class; + if (class_exists("ADODB_" . $class)) { + return $class; + } + + //ADOConnection::outp(adodb_pr(get_declared_classes(),true)); + if (!file_exists($file)) { + ADOConnection::outp("Missing file: $file"); + } else { + ADOConnection::outp("Syntax error in file: $file"); + } + return false; + } + + /** + * synonym for ADONewConnection for people like me who cannot remember the correct name + */ + function NewADOConnection($db='') { + $tmp = ADONewConnection($db); + return $tmp; + } + + /** + * Instantiate a new Connection class for a specific database driver. + * + * @param [db] is the database Connection object to create. If undefined, + * use the last database driver that was loaded by ADOLoadCode(). + * + * @return the freshly created instance of the Connection class. + */ + function ADONewConnection($db='') { + global $ADODB_NEWCONNECTION, $ADODB_LASTDB; + + if (!defined('ADODB_ASSOC_CASE')) { + define('ADODB_ASSOC_CASE', ADODB_ASSOC_CASE_NATIVE); + } + $errorfn = (defined('ADODB_ERROR_HANDLER')) ? ADODB_ERROR_HANDLER : false; + if (($at = strpos($db,'://')) !== FALSE) { + $origdsn = $db; + $fakedsn = 'fake'.substr($origdsn,$at); + if (($at2 = strpos($origdsn,'@/')) !== FALSE) { + // special handling of oracle, which might not have host + $fakedsn = str_replace('@/','@adodb-fakehost/',$fakedsn); + } + + if ((strpos($origdsn, 'sqlite')) !== FALSE && stripos($origdsn, '%2F') === FALSE) { + // special handling for SQLite, it only might have the path to the database file. + // If you try to connect to a SQLite database using a dsn + // like 'sqlite:///path/to/database', the 'parse_url' php function + // will throw you an exception with a message such as "unable to parse url" + list($scheme, $path) = explode('://', $origdsn); + $dsna['scheme'] = $scheme; + if ($qmark = strpos($path,'?')) { + $dsn['query'] = substr($path,$qmark+1); + $path = substr($path,0,$qmark); + } + $dsna['path'] = '/' . urlencode($path); + } else + $dsna = @parse_url($fakedsn); + + if (!$dsna) { + return false; + } + $dsna['scheme'] = substr($origdsn,0,$at); + if ($at2 !== FALSE) { + $dsna['host'] = ''; + } + + if (strncmp($origdsn,'pdo',3) == 0) { + $sch = explode('_',$dsna['scheme']); + if (sizeof($sch)>1) { + $dsna['host'] = isset($dsna['host']) ? rawurldecode($dsna['host']) : ''; + if ($sch[1] == 'sqlite') { + $dsna['host'] = rawurlencode($sch[1].':'.rawurldecode($dsna['host'])); + } else { + $dsna['host'] = rawurlencode($sch[1].':host='.rawurldecode($dsna['host'])); + } + $dsna['scheme'] = 'pdo'; + } + } + + $db = @$dsna['scheme']; + if (!$db) { + return false; + } + $dsna['host'] = isset($dsna['host']) ? rawurldecode($dsna['host']) : ''; + $dsna['user'] = isset($dsna['user']) ? rawurldecode($dsna['user']) : ''; + $dsna['pass'] = isset($dsna['pass']) ? rawurldecode($dsna['pass']) : ''; + $dsna['path'] = isset($dsna['path']) ? rawurldecode(substr($dsna['path'],1)) : ''; # strip off initial / + + if (isset($dsna['query'])) { + $opt1 = explode('&',$dsna['query']); + foreach($opt1 as $k => $v) { + $arr = explode('=',$v); + $opt[$arr[0]] = isset($arr[1]) ? rawurldecode($arr[1]) : 1; + } + } else { + $opt = array(); + } + } + /* + * phptype: Database backend used in PHP (mysql, odbc etc.) + * dbsyntax: Database used with regards to SQL syntax etc. + * protocol: Communication protocol to use (tcp, unix etc.) + * hostspec: Host specification (hostname[:port]) + * database: Database to use on the DBMS server + * username: User name for login + * password: Password for login + */ + if (!empty($ADODB_NEWCONNECTION)) { + $obj = $ADODB_NEWCONNECTION($db); + + } + + if(empty($obj)) { + + if (!isset($ADODB_LASTDB)) { + $ADODB_LASTDB = ''; + } + if (empty($db)) { + $db = $ADODB_LASTDB; + } + if ($db != $ADODB_LASTDB) { + $db = ADOLoadCode($db); + } + + if (!$db) { + if (isset($origdsn)) { + $db = $origdsn; + } + if ($errorfn) { + // raise an error + $ignore = false; + $errorfn('ADONewConnection', 'ADONewConnection', -998, + "could not load the database driver for '$db'", + $db,false,$ignore); + } else { + ADOConnection::outp( "

ADONewConnection: Unable to load database driver '$db'

",false); + } + return false; + } + + $cls = 'ADODB_'.$db; + if (!class_exists($cls)) { + adodb_backtrace(); + return false; + } + + $obj = new $cls(); + } + + # constructor should not fail + if ($obj) { + if ($errorfn) { + $obj->raiseErrorFn = $errorfn; + } + if (isset($dsna)) { + if (isset($dsna['port'])) { + $obj->port = $dsna['port']; + } + foreach($opt as $k => $v) { + switch(strtolower($k)) { + case 'new': + $nconnect = true; $persist = true; break; + case 'persist': + case 'persistent': $persist = $v; break; + case 'debug': $obj->debug = (integer) $v; break; + #ibase + case 'role': $obj->role = $v; break; + case 'dialect': $obj->dialect = (integer) $v; break; + case 'charset': $obj->charset = $v; $obj->charSet=$v; break; + case 'buffers': $obj->buffers = $v; break; + case 'fetchmode': $obj->SetFetchMode($v); break; + #ado + case 'charpage': $obj->charPage = $v; break; + #mysql, mysqli + case 'clientflags': $obj->clientFlags = $v; break; + #mysql, mysqli, postgres + case 'port': $obj->port = $v; break; + #mysqli + case 'socket': $obj->socket = $v; break; + #oci8 + case 'nls_date_format': $obj->NLS_DATE_FORMAT = $v; break; + case 'cachesecs': $obj->cacheSecs = $v; break; + case 'memcache': + $varr = explode(':',$v); + $vlen = sizeof($varr); + if ($vlen == 0) { + break; + } + $obj->memCache = true; + $obj->memCacheHost = explode(',',$varr[0]); + if ($vlen == 1) { + break; + } + $obj->memCachePort = $varr[1]; + if ($vlen == 2) { + break; + } + $obj->memCacheCompress = $varr[2] ? true : false; + break; + } + } + if (empty($persist)) { + $ok = $obj->Connect($dsna['host'], $dsna['user'], $dsna['pass'], $dsna['path']); + } else if (empty($nconnect)) { + $ok = $obj->PConnect($dsna['host'], $dsna['user'], $dsna['pass'], $dsna['path']); + } else { + $ok = $obj->NConnect($dsna['host'], $dsna['user'], $dsna['pass'], $dsna['path']); + } + + if (!$ok) { + return false; + } + } + } + return $obj; + } + + + + // $perf == true means called by NewPerfMonitor(), otherwise for data dictionary + function _adodb_getdriver($provider,$drivername,$perf=false) { + switch ($provider) { + case 'odbtp': + if (strncmp('odbtp_',$drivername,6)==0) { + return substr($drivername,6); + } + case 'odbc' : + if (strncmp('odbc_',$drivername,5)==0) { + return substr($drivername,5); + } + case 'ado' : + if (strncmp('ado_',$drivername,4)==0) { + return substr($drivername,4); + } + case 'native': + break; + default: + return $provider; + } + + switch($drivername) { + case 'mysqlt': + case 'mysqli': + $drivername='mysql'; + break; + case 'postgres7': + case 'postgres8': + $drivername = 'postgres'; + break; + case 'firebird15': + $drivername = 'firebird'; + break; + case 'oracle': + $drivername = 'oci8'; + break; + case 'access': + if ($perf) { + $drivername = ''; + } + break; + case 'db2' : + case 'sapdb' : + break; + default: + $drivername = 'generic'; + break; + } + return $drivername; + } + + function NewPerfMonitor(&$conn) { + $drivername = _adodb_getdriver($conn->dataProvider,$conn->databaseType,true); + if (!$drivername || $drivername == 'generic') { + return false; + } + include_once(ADODB_DIR.'/adodb-perf.inc.php'); + @include_once(ADODB_DIR."/perf/perf-$drivername.inc.php"); + $class = "Perf_$drivername"; + if (!class_exists($class)) { + return false; + } + $perf = new $class($conn); + + return $perf; + } + + function NewDataDictionary(&$conn,$drivername=false) { + if (!$drivername) { + $drivername = _adodb_getdriver($conn->dataProvider,$conn->databaseType); + } + + include_once(ADODB_DIR.'/adodb-lib.inc.php'); + include_once(ADODB_DIR.'/adodb-datadict.inc.php'); + $path = ADODB_DIR."/datadict/datadict-$drivername.inc.php"; + + if (!file_exists($path)) { + ADOConnection::outp("Dictionary driver '$path' not available"); + return false; + } + include_once($path); + $class = "ADODB2_$drivername"; + $dict = new $class(); + $dict->dataProvider = $conn->dataProvider; + $dict->connection = $conn; + $dict->upperName = strtoupper($drivername); + $dict->quote = $conn->nameQuote; + if (!empty($conn->_connectionID)) { + $dict->serverInfo = $conn->ServerInfo(); + } + + return $dict; + } + + + + /* + Perform a print_r, with pre tags for better formatting. + */ + function adodb_pr($var,$as_string=false) { + if ($as_string) { + ob_start(); + } + + if (isset($_SERVER['HTTP_USER_AGENT'])) { + echo "
\n";print_r($var);echo "
\n"; + } else { + print_r($var); + } + + if ($as_string) { + $s = ob_get_contents(); + ob_end_clean(); + return $s; + } + } + + /* + Perform a stack-crawl and pretty print it. + + @param printOrArr Pass in a boolean to indicate print, or an $exception->trace array (assumes that print is true then). + @param levels Number of levels to display + */ + function adodb_backtrace($printOrArr=true,$levels=9999,$ishtml=null) { + global $ADODB_INCLUDED_LIB; + if (empty($ADODB_INCLUDED_LIB)) { + include(ADODB_DIR.'/adodb-lib.inc.php'); + } + return _adodb_backtrace($printOrArr,$levels,0,$ishtml); + } + +} diff --git a/app/vendor/adodb/adodb-php/composer.json b/app/vendor/adodb/adodb-php/composer.json new file mode 100644 index 000000000..21bd25f92 --- /dev/null +++ b/app/vendor/adodb/adodb-php/composer.json @@ -0,0 +1,37 @@ +{ + "name" : "adodb/adodb-php", + "description" : "ADOdb is a PHP database abstraction layer library", + "license" : [ "BSD-3-Clause", "LGPL-2.1" ], + "authors" : [ + { + "name": "John Lim", + "email" : "jlim@natsoft.com", + "role": "Author" + }, + { + "name": "Damien Regad", + "role": "Current maintainer" + }, + { + "name": "Mark Newnham", + "role": "Developer" + } + ], + + "keywords" : [ "database", "abstraction", "layer", "library", "php" ], + + "homepage": "http://adodb.org/", + "support" : { + "issues" : "https://github.com/ADOdb/ADOdb/issues", + "source" : "https://github.com/ADOdb/ADOdb" + }, + + "require" : { + "php" : ">=5.3.2" + }, + + "autoload" : { + "files" : ["adodb.inc.php"] + } + +} diff --git a/app/vendor/adodb/adodb-php/contrib/toxmlrpc.inc.php b/app/vendor/adodb/adodb-php/contrib/toxmlrpc.inc.php new file mode 100644 index 000000000..f769cc5fd --- /dev/null +++ b/app/vendor/adodb/adodb-php/contrib/toxmlrpc.inc.php @@ -0,0 +1,181 @@ +GetArray()) would work with: + * - ADODB_FETCH_BOTH + * - null values + */ + + /** + * Include the main libraries + */ + require_once('xmlrpc.inc'); + if (!defined('ADODB_DIR')) require_once('adodb.inc.php'); + + /** + * Builds an xmlrpc struct value out of an AdoDB recordset + */ + function rs2xmlrpcval(&$adodbrs) { + + $header = rs2xmlrpcval_header($adodbrs); + $body = rs2xmlrpcval_body($adodbrs); + + // put it all together and build final xmlrpc struct + $xmlrpcrs = new xmlrpcval ( array( + "header" => $header, + "body" => $body, + ), "struct"); + + return $xmlrpcrs; + + } + + /** + * Builds an xmlrpc struct value describing an AdoDB recordset + */ + function rs2xmlrpcval_header($adodbrs) + { + $numfields = $adodbrs->FieldCount(); + $numrecords = $adodbrs->RecordCount(); + + // build structure holding recordset information + $fieldstruct = array(); + for ($i = 0; $i < $numfields; $i++) { + $fld = $adodbrs->FetchField($i); + $fieldarray = array(); + if (isset($fld->name)) + $fieldarray["name"] = new xmlrpcval ($fld->name); + if (isset($fld->type)) + $fieldarray["type"] = new xmlrpcval ($fld->type); + if (isset($fld->max_length)) + $fieldarray["max_length"] = new xmlrpcval ($fld->max_length, "int"); + if (isset($fld->not_null)) + $fieldarray["not_null"] = new xmlrpcval ($fld->not_null, "boolean"); + if (isset($fld->has_default)) + $fieldarray["has_default"] = new xmlrpcval ($fld->has_default, "boolean"); + if (isset($fld->default_value)) + $fieldarray["default_value"] = new xmlrpcval ($fld->default_value); + $fieldstruct[$i] = new xmlrpcval ($fieldarray, "struct"); + } + $fieldcount = new xmlrpcval ($numfields, "int"); + $recordcount = new xmlrpcval ($numrecords, "int"); + $sql = new xmlrpcval ($adodbrs->sql); + $fieldinfo = new xmlrpcval ($fieldstruct, "array"); + + $header = new xmlrpcval ( array( + "fieldcount" => $fieldcount, + "recordcount" => $recordcount, + "sql" => $sql, + "fieldinfo" => $fieldinfo + ), "struct"); + + return $header; + } + + /** + * Builds an xmlrpc struct value out of an AdoDB recordset + * (data values only, no data definition) + */ + function rs2xmlrpcval_body($adodbrs) + { + $numfields = $adodbrs->FieldCount(); + + // build structure containing recordset data + $adodbrs->MoveFirst(); + $rows = array(); + while (!$adodbrs->EOF) { + $columns = array(); + // This should work on all cases of fetch mode: assoc, num, both or default + if ($adodbrs->fetchMode == 'ADODB_FETCH_BOTH' || count($adodbrs->fields) == 2 * $adodbrs->FieldCount()) + for ($i = 0; $i < $numfields; $i++) + if ($adodbrs->fields[$i] === null) + $columns[$i] = new xmlrpcval (''); + else + $columns[$i] = xmlrpc_encode ($adodbrs->fields[$i]); + else + foreach ($adodbrs->fields as $val) + if ($val === null) + $columns[] = new xmlrpcval (''); + else + $columns[] = xmlrpc_encode ($val); + + $rows[] = new xmlrpcval ($columns, "array"); + + $adodbrs->MoveNext(); + } + $body = new xmlrpcval ($rows, "array"); + + return $body; + } + + /** + * Returns an xmlrpc struct value as string out of an AdoDB recordset + */ + function rs2xmlrpcstring (&$adodbrs) { + $xmlrpc = rs2xmlrpcval ($adodbrs); + if ($xmlrpc) + return $xmlrpc->serialize(); + else + return null; + } + + /** + * Given a well-formed xmlrpc struct object returns an AdoDB object + * + * @todo add some error checking on the input value + */ + function xmlrpcval2rs (&$xmlrpcval) { + + $fields_array = array(); + $data_array = array(); + + // rebuild column information + $header = $xmlrpcval->structmem('header'); + + $numfields = $header->structmem('fieldcount'); + $numfields = $numfields->scalarval(); + $numrecords = $header->structmem('recordcount'); + $numrecords = $numrecords->scalarval(); + $sqlstring = $header->structmem('sql'); + $sqlstring = $sqlstring->scalarval(); + + $fieldinfo = $header->structmem('fieldinfo'); + for ($i = 0; $i < $numfields; $i++) { + $temp = $fieldinfo->arraymem($i); + $fld = new ADOFieldObject(); + while (list($key,$value) = $temp->structeach()) { + if ($key == "name") $fld->name = $value->scalarval(); + if ($key == "type") $fld->type = $value->scalarval(); + if ($key == "max_length") $fld->max_length = $value->scalarval(); + if ($key == "not_null") $fld->not_null = $value->scalarval(); + if ($key == "has_default") $fld->has_default = $value->scalarval(); + if ($key == "default_value") $fld->default_value = $value->scalarval(); + } // while + $fields_array[] = $fld; + } // for + + // fetch recordset information into php array + $body = $xmlrpcval->structmem('body'); + for ($i = 0; $i < $numrecords; $i++) { + $data_array[$i]= array(); + $xmlrpcrs_row = $body->arraymem($i); + for ($j = 0; $j < $numfields; $j++) { + $temp = $xmlrpcrs_row->arraymem($j); + $data_array[$i][$j] = $temp->scalarval(); + } // for j + } // for i + + // finally build in-memory recordset object and return it + $rs = new ADORecordSet_array(); + $rs->InitArrayFields($data_array,$fields_array); + return $rs; + + } diff --git a/app/vendor/adodb/adodb-php/cute_icons_for_site/adodb.gif b/app/vendor/adodb/adodb-php/cute_icons_for_site/adodb.gif new file mode 100644 index 000000000..c5e8dfc6d Binary files /dev/null and b/app/vendor/adodb/adodb-php/cute_icons_for_site/adodb.gif differ diff --git a/app/vendor/adodb/adodb-php/cute_icons_for_site/adodb2.gif b/app/vendor/adodb/adodb-php/cute_icons_for_site/adodb2.gif new file mode 100644 index 000000000..f12ae2037 Binary files /dev/null and b/app/vendor/adodb/adodb-php/cute_icons_for_site/adodb2.gif differ diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-access.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-access.inc.php new file mode 100644 index 000000000..c14591543 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-access.inc.php @@ -0,0 +1,95 @@ +debug) ADOConnection::outp("Warning: Access does not supported DEFAULT values (field $fname)"); + } + if ($fnotnull) $suffix .= ' NOT NULL'; + if ($fconstraint) $suffix .= ' '.$fconstraint; + return $suffix; + } + + function CreateDatabase($dbname,$options=false) + { + return array(); + } + + + function SetSchema($schema) + { + } + + function AlterColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + if ($this->debug) ADOConnection::outp("AlterColumnSQL not supported"); + return array(); + } + + + function DropColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + if ($this->debug) ADOConnection::outp("DropColumnSQL not supported"); + return array(); + } + +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-db2.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-db2.inc.php new file mode 100644 index 000000000..7d201ff17 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-db2.inc.php @@ -0,0 +1,143 @@ +debug) ADOConnection::outp("AlterColumnSQL not supported"); + return array(); + } + + + function DropColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + if ($this->debug) ADOConnection::outp("DropColumnSQL not supported"); + return array(); + } + + + function ChangeTableSQL($tablename, $flds, $tableoptions = false) + { + + /** + Allow basic table changes to DB2 databases + DB2 will fatally reject changes to non character columns + + */ + + $validTypes = array("CHAR","VARC"); + $invalidTypes = array("BIGI","BLOB","CLOB","DATE", "DECI","DOUB", "INTE", "REAL","SMAL", "TIME"); + // check table exists + $cols = $this->MetaColumns($tablename); + if ( empty($cols)) { + return $this->CreateTableSQL($tablename, $flds, $tableoptions); + } + + // already exists, alter table instead + list($lines,$pkey) = $this->_GenFields($flds); + $alter = 'ALTER TABLE ' . $this->TableName($tablename); + $sql = array(); + + foreach ( $lines as $id => $v ) { + if ( isset($cols[$id]) && is_object($cols[$id]) ) { + /** + If the first field of $v is the fieldname, and + the second is the field type/size, we assume its an + attempt to modify the column size, so check that it is allowed + $v can have an indeterminate number of blanks between the + fields, so account for that too + */ + $vargs = explode(' ' , $v); + // assume that $vargs[0] is the field name. + $i=0; + // Find the next non-blank value; + for ($i=1;$ialterCol . ' ' . $v; + } else { + $sql[] = $alter . $this->addCol . ' ' . $v; + } + } + + return $sql; + } + +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-firebird.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-firebird.inc.php new file mode 100644 index 000000000..f247c8dc0 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-firebird.inc.php @@ -0,0 +1,151 @@ +connection) ) { + return $name; + } + + $quote = $this->connection->nameQuote; + + // if name is of the form `name`, quote it + if ( preg_match('/^`(.+)`$/', $name, $matches) ) { + return $quote . $matches[1] . $quote; + } + + // if name contains special characters, quote it + if ( !preg_match('/^[' . $this->nameRegex . ']+$/', $name) ) { + return $quote . $name . $quote; + } + + return $quote . $name . $quote; + } + + function CreateDatabase($dbname, $options=false) + { + $options = $this->_Options($options); + $sql = array(); + + $sql[] = "DECLARE EXTERNAL FUNCTION LOWER CSTRING(80) RETURNS CSTRING(80) FREE_IT ENTRY_POINT 'IB_UDF_lower' MODULE_NAME 'ib_udf'"; + + return $sql; + } + + function _DropAutoIncrement($t) + { + if (strpos($t,'.') !== false) { + $tarr = explode('.',$t); + return 'DROP GENERATOR '.$tarr[0].'."gen_'.$tarr[1].'"'; + } + return 'DROP GENERATOR "GEN_'.$t; + } + + + function _CreateSuffix($fname,&$ftype,$fnotnull,$fdefault,$fautoinc,$fconstraint,$funsigned) + { + $suffix = ''; + + if (strlen($fdefault)) $suffix .= " DEFAULT $fdefault"; + if ($fnotnull) $suffix .= ' NOT NULL'; + if ($fautoinc) $this->seqField = $fname; + if ($fconstraint) $suffix .= ' '.$fconstraint; + + return $suffix; + } + +/* +CREATE or replace TRIGGER jaddress_insert +before insert on jaddress +for each row +begin +IF ( NEW."seqField" IS NULL OR NEW."seqField" = 0 ) THEN + NEW."seqField" = GEN_ID("GEN_tabname", 1); +end; +*/ + function _Triggers($tabname,$tableoptions) + { + if (!$this->seqField) return array(); + + $tab1 = preg_replace( '/"/', '', $tabname ); + if ($this->schema) { + $t = strpos($tab1,'.'); + if ($t !== false) $tab = substr($tab1,$t+1); + else $tab = $tab1; + $seqField = $this->seqField; + $seqname = $this->schema.'.'.$this->seqPrefix.$tab; + $trigname = $this->schema.'.trig_'.$this->seqPrefix.$tab; + } else { + $seqField = $this->seqField; + $seqname = $this->seqPrefix.$tab1; + $trigname = 'trig_'.$seqname; + } + if (isset($tableoptions['REPLACE'])) + { $sql[] = "DROP GENERATOR \"$seqname\""; + $sql[] = "CREATE GENERATOR \"$seqname\""; + $sql[] = "ALTER TRIGGER \"$trigname\" BEFORE INSERT OR UPDATE AS BEGIN IF ( NEW.$seqField IS NULL OR NEW.$seqField = 0 ) THEN NEW.$seqField = GEN_ID(\"$seqname\", 1); END"; + } + else + { $sql[] = "CREATE GENERATOR \"$seqname\""; + $sql[] = "CREATE TRIGGER \"$trigname\" FOR $tabname BEFORE INSERT OR UPDATE AS BEGIN IF ( NEW.$seqField IS NULL OR NEW.$seqField = 0 ) THEN NEW.$seqField = GEN_ID(\"$seqname\", 1); END"; + } + + $this->seqField = false; + return $sql; + } + +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-generic.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-generic.inc.php new file mode 100644 index 000000000..e2e6909e7 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-generic.inc.php @@ -0,0 +1,127 @@ +debug) ADOConnection::outp("AlterColumnSQL not supported"); + return array(); + } + + + function DropColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + if ($this->debug) ADOConnection::outp("DropColumnSQL not supported"); + return array(); + } + +} + +/* +//db2 + function ActualType($meta) + { + switch($meta) { + case 'C': return 'VARCHAR'; + case 'X': return 'VARCHAR'; + + case 'C2': return 'VARCHAR'; // up to 32K + case 'X2': return 'VARCHAR'; + + case 'B': return 'BLOB'; + + case 'D': return 'DATE'; + case 'T': return 'TIMESTAMP'; + + case 'L': return 'SMALLINT'; + case 'I': return 'INTEGER'; + case 'I1': return 'SMALLINT'; + case 'I2': return 'SMALLINT'; + case 'I4': return 'INTEGER'; + case 'I8': return 'BIGINT'; + + case 'F': return 'DOUBLE'; + case 'N': return 'DECIMAL'; + default: + return $meta; + } + } + +// ifx +function ActualType($meta) + { + switch($meta) { + case 'C': return 'VARCHAR';// 255 + case 'X': return 'TEXT'; + + case 'C2': return 'NVARCHAR'; + case 'X2': return 'TEXT'; + + case 'B': return 'BLOB'; + + case 'D': return 'DATE'; + case 'T': return 'DATETIME'; + + case 'L': return 'SMALLINT'; + case 'I': return 'INTEGER'; + case 'I1': return 'SMALLINT'; + case 'I2': return 'SMALLINT'; + case 'I4': return 'INTEGER'; + case 'I8': return 'DECIMAL(20)'; + + case 'F': return 'FLOAT'; + case 'N': return 'DECIMAL'; + default: + return $meta; + } + } +*/ diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-ibase.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-ibase.inc.php new file mode 100644 index 000000000..495a722d6 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-ibase.inc.php @@ -0,0 +1,67 @@ +debug) ADOConnection::outp("AlterColumnSQL not supported"); + return array(); + } + + + function DropColumnSQL($tabname, $flds, $tableflds='', $tableoptions='') + { + if ($this->debug) ADOConnection::outp("DropColumnSQL not supported"); + return array(); + } + +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-informix.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-informix.inc.php new file mode 100644 index 000000000..25726f499 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-informix.inc.php @@ -0,0 +1,81 @@ +debug) ADOConnection::outp("AlterColumnSQL not supported"); + return array(); + } + + + function DropColumnSQL($tabname, $flds, $tableflds='', $tableoptions='') + { + if ($this->debug) ADOConnection::outp("DropColumnSQL not supported"); + return array(); + } + + // return string must begin with space + function _CreateSuffix($fname, &$ftype, $fnotnull,$fdefault,$fautoinc,$fconstraint,$funsigned) + { + if ($fautoinc) { + $ftype = 'SERIAL'; + return ''; + } + $suffix = ''; + if (strlen($fdefault)) $suffix .= " DEFAULT $fdefault"; + if ($fnotnull) $suffix .= ' NOT NULL'; + if ($fconstraint) $suffix .= ' '.$fconstraint; + return $suffix; + } + +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-mssql.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-mssql.inc.php new file mode 100644 index 000000000..2c496dea6 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-mssql.inc.php @@ -0,0 +1,285 @@ +type; + $len = $fieldobj->max_length; + } + + $len = -1; // mysql max_length is not accurate + switch (strtoupper($t)) { + case 'R': + case 'INT': + case 'INTEGER': return 'I'; + case 'BIT': + case 'TINYINT': return 'I1'; + case 'SMALLINT': return 'I2'; + case 'BIGINT': return 'I8'; + case 'SMALLDATETIME': return 'T'; + case 'REAL': + case 'FLOAT': return 'F'; + default: return parent::MetaType($t,$len,$fieldobj); + } + } + + function ActualType($meta) + { + switch(strtoupper($meta)) { + + case 'C': return 'VARCHAR'; + case 'XL': return (isset($this)) ? $this->typeXL : 'TEXT'; + case 'X': return (isset($this)) ? $this->typeX : 'TEXT'; ## could be varchar(8000), but we want compat with oracle + case 'C2': return 'NVARCHAR'; + case 'X2': return 'NTEXT'; + + case 'B': return 'IMAGE'; + + case 'D': return 'DATETIME'; + + case 'TS': + case 'T': return 'DATETIME'; + case 'L': return 'BIT'; + + case 'R': + case 'I': return 'INT'; + case 'I1': return 'TINYINT'; + case 'I2': return 'SMALLINT'; + case 'I4': return 'INT'; + case 'I8': return 'BIGINT'; + + case 'F': return 'REAL'; + case 'N': return 'NUMERIC'; + default: + return $meta; + } + } + + + function AddColumnSQL($tabname, $flds) + { + $tabname = $this->TableName ($tabname); + $f = array(); + list($lines,$pkey) = $this->_GenFields($flds); + $s = "ALTER TABLE $tabname $this->addCol"; + foreach($lines as $v) { + $f[] = "\n $v"; + } + $s .= implode(', ',$f); + $sql[] = $s; + return $sql; + } + + /* + function AlterColumnSQL($tabname, $flds, $tableflds='', $tableoptions='') + { + $tabname = $this->TableName ($tabname); + $sql = array(); + list($lines,$pkey) = $this->_GenFields($flds); + foreach($lines as $v) { + $sql[] = "ALTER TABLE $tabname $this->alterCol $v"; + } + + return $sql; + } + */ + + function DropColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + $tabname = $this->TableName ($tabname); + if (!is_array($flds)) + $flds = explode(',',$flds); + $f = array(); + $s = 'ALTER TABLE ' . $tabname; + foreach($flds as $v) { + $f[] = "\n$this->dropCol ".$this->NameQuote($v); + } + $s .= implode(', ',$f); + $sql[] = $s; + return $sql; + } + + // return string must begin with space + function _CreateSuffix($fname,&$ftype,$fnotnull,$fdefault,$fautoinc,$fconstraint,$funsigned) + { + $suffix = ''; + if (strlen($fdefault)) $suffix .= " DEFAULT $fdefault"; + if ($fautoinc) $suffix .= ' IDENTITY(1,1)'; + if ($fnotnull) $suffix .= ' NOT NULL'; + else if ($suffix == '') $suffix .= ' NULL'; + if ($fconstraint) $suffix .= ' '.$fconstraint; + return $suffix; + } + + /* +CREATE TABLE + [ database_name.[ owner ] . | owner. ] table_name + ( { < column_definition > + | column_name AS computed_column_expression + | < table_constraint > ::= [ CONSTRAINT constraint_name ] } + + | [ { PRIMARY KEY | UNIQUE } [ ,...n ] + ) + +[ ON { filegroup | DEFAULT } ] +[ TEXTIMAGE_ON { filegroup | DEFAULT } ] + +< column_definition > ::= { column_name data_type } + [ COLLATE < collation_name > ] + [ [ DEFAULT constant_expression ] + | [ IDENTITY [ ( seed , increment ) [ NOT FOR REPLICATION ] ] ] + ] + [ ROWGUIDCOL] + [ < column_constraint > ] [ ...n ] + +< column_constraint > ::= [ CONSTRAINT constraint_name ] + { [ NULL | NOT NULL ] + | [ { PRIMARY KEY | UNIQUE } + [ CLUSTERED | NONCLUSTERED ] + [ WITH FILLFACTOR = fillfactor ] + [ON {filegroup | DEFAULT} ] ] + ] + | [ [ FOREIGN KEY ] + REFERENCES ref_table [ ( ref_column ) ] + [ ON DELETE { CASCADE | NO ACTION } ] + [ ON UPDATE { CASCADE | NO ACTION } ] + [ NOT FOR REPLICATION ] + ] + | CHECK [ NOT FOR REPLICATION ] + ( logical_expression ) + } + +< table_constraint > ::= [ CONSTRAINT constraint_name ] + { [ { PRIMARY KEY | UNIQUE } + [ CLUSTERED | NONCLUSTERED ] + { ( column [ ASC | DESC ] [ ,...n ] ) } + [ WITH FILLFACTOR = fillfactor ] + [ ON { filegroup | DEFAULT } ] + ] + | FOREIGN KEY + [ ( column [ ,...n ] ) ] + REFERENCES ref_table [ ( ref_column [ ,...n ] ) ] + [ ON DELETE { CASCADE | NO ACTION } ] + [ ON UPDATE { CASCADE | NO ACTION } ] + [ NOT FOR REPLICATION ] + | CHECK [ NOT FOR REPLICATION ] + ( search_conditions ) + } + + + */ + + /* + CREATE [ UNIQUE ] [ CLUSTERED | NONCLUSTERED ] INDEX index_name + ON { table | view } ( column [ ASC | DESC ] [ ,...n ] ) + [ WITH < index_option > [ ,...n] ] + [ ON filegroup ] + < index_option > :: = + { PAD_INDEX | + FILLFACTOR = fillfactor | + IGNORE_DUP_KEY | + DROP_EXISTING | + STATISTICS_NORECOMPUTE | + SORT_IN_TEMPDB + } +*/ + function _IndexSQL($idxname, $tabname, $flds, $idxoptions) + { + $sql = array(); + + if ( isset($idxoptions['REPLACE']) || isset($idxoptions['DROP']) ) { + $sql[] = sprintf ($this->dropIndex, $idxname, $tabname); + if ( isset($idxoptions['DROP']) ) + return $sql; + } + + if ( empty ($flds) ) { + return $sql; + } + + $unique = isset($idxoptions['UNIQUE']) ? ' UNIQUE' : ''; + $clustered = isset($idxoptions['CLUSTERED']) ? ' CLUSTERED' : ''; + + if ( is_array($flds) ) + $flds = implode(', ',$flds); + $s = 'CREATE' . $unique . $clustered . ' INDEX ' . $idxname . ' ON ' . $tabname . ' (' . $flds . ')'; + + if ( isset($idxoptions[$this->upperName]) ) + $s .= $idxoptions[$this->upperName]; + + + $sql[] = $s; + + return $sql; + } + + + function _GetSize($ftype, $ty, $fsize, $fprec) + { + switch ($ftype) { + case 'INT': + case 'SMALLINT': + case 'TINYINT': + case 'BIGINT': + return $ftype; + } + if ($ty == 'T') return $ftype; + return parent::_GetSize($ftype, $ty, $fsize, $fprec); + + } +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-mssqlnative.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-mssqlnative.inc.php new file mode 100644 index 000000000..36d5fcad4 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-mssqlnative.inc.php @@ -0,0 +1,369 @@ +type; + $len = $fieldobj->max_length; + } + + $_typeConversion = array( + -155 => 'D', + 93 => 'D', + -154 => 'D', + -2 => 'D', + 91 => 'D', + + 12 => 'C', + 1 => 'C', + -9 => 'C', + -8 => 'C', + + -7 => 'L', + -6 => 'I2', + -5 => 'I8', + -11 => 'I', + 4 => 'I', + 5 => 'I4', + + -1 => 'X', + -10 => 'X', + + 2 => 'N', + 3 => 'N', + 6 => 'N', + 7 => 'N', + + -152 => 'X', + -151 => 'X', + -4 => 'X', + -3 => 'X' + ); + + return $_typeConversion($t); + + } + + function ActualType($meta) + { + $DATE_TYPE = 'DATETIME'; + + switch(strtoupper($meta)) { + + case 'C': return 'VARCHAR'; + case 'XL': return (isset($this)) ? $this->typeXL : 'TEXT'; + case 'X': return (isset($this)) ? $this->typeX : 'TEXT'; ## could be varchar(8000), but we want compat with oracle + case 'C2': return 'NVARCHAR'; + case 'X2': return 'NTEXT'; + + case 'B': return 'IMAGE'; + + case 'D': return $DATE_TYPE; + case 'T': return 'TIME'; + case 'L': return 'BIT'; + + case 'R': + case 'I': return 'INT'; + case 'I1': return 'TINYINT'; + case 'I2': return 'SMALLINT'; + case 'I4': return 'INT'; + case 'I8': return 'BIGINT'; + + case 'F': return 'REAL'; + case 'N': return 'NUMERIC'; + default: + print "RETURN $meta"; + return $meta; + } + } + + + function AddColumnSQL($tabname, $flds) + { + $tabname = $this->TableName ($tabname); + $f = array(); + list($lines,$pkey) = $this->_GenFields($flds); + $s = "ALTER TABLE $tabname $this->addCol"; + foreach($lines as $v) { + $f[] = "\n $v"; + } + $s .= implode(', ',$f); + $sql[] = $s; + return $sql; + } + + function DefaultConstraintname($tabname, $colname) + { + $constraintname = false; + $rs = $this->connection->Execute( + "SELECT name FROM sys.default_constraints + WHERE object_name(parent_object_id) = '$tabname' + AND col_name(parent_object_id, parent_column_id) = '$colname'" + ); + if ( is_object($rs) ) { + $row = $rs->FetchRow(); + $constraintname = $row['name']; + } + return $constraintname; + } + + function AlterColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + $tabname = $this->TableName ($tabname); + $sql = array(); + + list($lines,$pkey,$idxs) = $this->_GenFields($flds); + $alter = 'ALTER TABLE ' . $tabname . $this->alterCol . ' '; + foreach($lines as $v) { + $not_null = false; + if ($not_null = preg_match('/NOT NULL/i',$v)) { + $v = preg_replace('/NOT NULL/i','',$v); + } + if (preg_match('/^([^ ]+) .*DEFAULT (\'[^\']+\'|\"[^\"]+\"|[^ ]+)/',$v,$matches)) { + list(,$colname,$default) = $matches; + $v = preg_replace('/^' . preg_quote($colname) . '\s/', '', $v); + $t = trim(str_replace('DEFAULT '.$default,'',$v)); + if ( $constraintname = $this->DefaultConstraintname($tabname,$colname) ) { + $sql[] = 'ALTER TABLE '.$tabname.' DROP CONSTRAINT '. $constraintname; + } + if ($not_null) { + $sql[] = $alter . $colname . ' ' . $t . ' NOT NULL'; + } else { + $sql[] = $alter . $colname . ' ' . $t ; + } + $sql[] = 'ALTER TABLE ' . $tabname + . ' ADD CONSTRAINT DF__' . $tabname . '__' . $colname . '__' . dechex(rand()) + . ' DEFAULT ' . $default . ' FOR ' . $colname; + } else { + $colname = strtok($v," "); + if ( $constraintname = $this->DefaultConstraintname($tabname,$colname) ) { + $sql[] = 'ALTER TABLE '.$tabname.' DROP CONSTRAINT '. $constraintname; + } + if ($not_null) { + $sql[] = $alter . $v . ' NOT NULL'; + } else { + $sql[] = $alter . $v; + } + } + } + if (is_array($idxs)) { + foreach($idxs as $idx => $idxdef) { + $sql_idxs = $this->CreateIndexSql($idx, $tabname, $idxdef['cols'], $idxdef['opts']); + $sql = array_merge($sql, $sql_idxs); + } + } + return $sql; + } + + + /** + * Drop a column, syntax is ALTER TABLE table DROP COLUMN column,column + * + * @param string $tabname Table Name + * @param string[] $flds One, or an array of Fields To Drop + * @param string $tableflds Throwaway value to make the function match the parent + * @param string $tableoptions Throway value to make the function match the parent + * + * @return string The SQL necessary to drop the column + */ + function DropColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + $tabname = $this->TableName ($tabname); + if (!is_array($flds)) + $flds = explode(',',$flds); + $f = array(); + $s = 'ALTER TABLE ' . $tabname; + foreach($flds as $v) { + if ( $constraintname = $this->DefaultConstraintname($tabname,$v) ) { + $sql[] = 'ALTER TABLE ' . $tabname . ' DROP CONSTRAINT ' . $constraintname; + } + $f[] = ' DROP COLUMN ' . $this->NameQuote($v); + } + $s .= implode(', ',$f); + $sql[] = $s; + return $sql; + } + + // return string must begin with space + function _CreateSuffix($fname,&$ftype,$fnotnull,$fdefault,$fautoinc,$fconstraint,$funsigned) + { + $suffix = ''; + if (strlen($fdefault)) $suffix .= " DEFAULT $fdefault"; + if ($fautoinc) $suffix .= ' IDENTITY(1,1)'; + if ($fnotnull) $suffix .= ' NOT NULL'; + else if ($suffix == '') $suffix .= ' NULL'; + if ($fconstraint) $suffix .= ' '.$fconstraint; + return $suffix; + } + + /* +CREATE TABLE + [ database_name.[ owner ] . | owner. ] table_name + ( { < column_definition > + | column_name AS computed_column_expression + | < table_constraint > ::= [ CONSTRAINT constraint_name ] } + + | [ { PRIMARY KEY | UNIQUE } [ ,...n ] + ) + +[ ON { filegroup | DEFAULT } ] +[ TEXTIMAGE_ON { filegroup | DEFAULT } ] + +< column_definition > ::= { column_name data_type } + [ COLLATE < collation_name > ] + [ [ DEFAULT constant_expression ] + | [ IDENTITY [ ( seed , increment ) [ NOT FOR REPLICATION ] ] ] + ] + [ ROWGUIDCOL] + [ < column_constraint > ] [ ...n ] + +< column_constraint > ::= [ CONSTRAINT constraint_name ] + { [ NULL | NOT NULL ] + | [ { PRIMARY KEY | UNIQUE } + [ CLUSTERED | NONCLUSTERED ] + [ WITH FILLFACTOR = fillfactor ] + [ON {filegroup | DEFAULT} ] ] + ] + | [ [ FOREIGN KEY ] + REFERENCES ref_table [ ( ref_column ) ] + [ ON DELETE { CASCADE | NO ACTION } ] + [ ON UPDATE { CASCADE | NO ACTION } ] + [ NOT FOR REPLICATION ] + ] + | CHECK [ NOT FOR REPLICATION ] + ( logical_expression ) + } + +< table_constraint > ::= [ CONSTRAINT constraint_name ] + { [ { PRIMARY KEY | UNIQUE } + [ CLUSTERED | NONCLUSTERED ] + { ( column [ ASC | DESC ] [ ,...n ] ) } + [ WITH FILLFACTOR = fillfactor ] + [ ON { filegroup | DEFAULT } ] + ] + | FOREIGN KEY + [ ( column [ ,...n ] ) ] + REFERENCES ref_table [ ( ref_column [ ,...n ] ) ] + [ ON DELETE { CASCADE | NO ACTION } ] + [ ON UPDATE { CASCADE | NO ACTION } ] + [ NOT FOR REPLICATION ] + | CHECK [ NOT FOR REPLICATION ] + ( search_conditions ) + } + + + */ + + /* + CREATE [ UNIQUE ] [ CLUSTERED | NONCLUSTERED ] INDEX index_name + ON { table | view } ( column [ ASC | DESC ] [ ,...n ] ) + [ WITH < index_option > [ ,...n] ] + [ ON filegroup ] + < index_option > :: = + { PAD_INDEX | + FILLFACTOR = fillfactor | + IGNORE_DUP_KEY | + DROP_EXISTING | + STATISTICS_NORECOMPUTE | + SORT_IN_TEMPDB + } +*/ + function _IndexSQL($idxname, $tabname, $flds, $idxoptions) + { + $sql = array(); + + if ( isset($idxoptions['REPLACE']) || isset($idxoptions['DROP']) ) { + $sql[] = sprintf ($this->dropIndex, $idxname, $tabname); + if ( isset($idxoptions['DROP']) ) + return $sql; + } + + if ( empty ($flds) ) { + return $sql; + } + + $unique = isset($idxoptions['UNIQUE']) ? ' UNIQUE' : ''; + $clustered = isset($idxoptions['CLUSTERED']) ? ' CLUSTERED' : ''; + + if ( is_array($flds) ) + $flds = implode(', ',$flds); + $s = 'CREATE' . $unique . $clustered . ' INDEX ' . $idxname . ' ON ' . $tabname . ' (' . $flds . ')'; + + if ( isset($idxoptions[$this->upperName]) ) + $s .= $idxoptions[$this->upperName]; + + + $sql[] = $s; + + return $sql; + } + + + function _GetSize($ftype, $ty, $fsize, $fprec) + { + switch ($ftype) { + case 'INT': + case 'SMALLINT': + case 'TINYINT': + case 'BIGINT': + return $ftype; + } + if ($ty == 'T') return $ftype; + return parent::_GetSize($ftype, $ty, $fsize, $fprec); + + } +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-mysql.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-mysql.inc.php new file mode 100644 index 000000000..00a43a2a1 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-mysql.inc.php @@ -0,0 +1,183 @@ +type; + $len = $fieldobj->max_length; + } + $is_serial = is_object($fieldobj) && $fieldobj->primary_key && $fieldobj->auto_increment; + + $len = -1; // mysql max_length is not accurate + switch (strtoupper($t)) { + case 'STRING': + case 'CHAR': + case 'VARCHAR': + case 'TINYBLOB': + case 'TINYTEXT': + case 'ENUM': + case 'SET': + if ($len <= $this->blobSize) return 'C'; + + case 'TEXT': + case 'LONGTEXT': + case 'MEDIUMTEXT': + return 'X'; + + // php_mysql extension always returns 'blob' even if 'text' + // so we have to check whether binary... + case 'IMAGE': + case 'LONGBLOB': + case 'BLOB': + case 'MEDIUMBLOB': + return !empty($fieldobj->binary) ? 'B' : 'X'; + + case 'YEAR': + case 'DATE': return 'D'; + + case 'TIME': + case 'DATETIME': + case 'TIMESTAMP': return 'T'; + + case 'FLOAT': + case 'DOUBLE': + return 'F'; + + case 'INT': + case 'INTEGER': return $is_serial ? 'R' : 'I'; + case 'TINYINT': return $is_serial ? 'R' : 'I1'; + case 'SMALLINT': return $is_serial ? 'R' : 'I2'; + case 'MEDIUMINT': return $is_serial ? 'R' : 'I4'; + case 'BIGINT': return $is_serial ? 'R' : 'I8'; + default: return 'N'; + } + } + + function ActualType($meta) + { + switch(strtoupper($meta)) { + case 'C': return 'VARCHAR'; + case 'XL':return 'LONGTEXT'; + case 'X': return 'TEXT'; + + case 'C2': return 'VARCHAR'; + case 'X2': return 'LONGTEXT'; + + case 'B': return 'LONGBLOB'; + + case 'D': return 'DATE'; + case 'TS': + case 'T': return 'DATETIME'; + case 'L': return 'TINYINT'; + + case 'R': + case 'I4': + case 'I': return 'INTEGER'; + case 'I1': return 'TINYINT'; + case 'I2': return 'SMALLINT'; + case 'I8': return 'BIGINT'; + + case 'F': return 'DOUBLE'; + case 'N': return 'NUMERIC'; + default: + return $meta; + } + } + + // return string must begin with space + function _CreateSuffix($fname,&$ftype,$fnotnull,$fdefault,$fautoinc,$fconstraint,$funsigned) + { + $suffix = ''; + if ($funsigned) $suffix .= ' UNSIGNED'; + if ($fnotnull) $suffix .= ' NOT NULL'; + if (strlen($fdefault)) $suffix .= " DEFAULT $fdefault"; + if ($fautoinc) $suffix .= ' AUTO_INCREMENT'; + if ($fconstraint) $suffix .= ' '.$fconstraint; + return $suffix; + } + + /* + CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name [(create_definition,...)] + [table_options] [select_statement] + create_definition: + col_name type [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] + [PRIMARY KEY] [reference_definition] + or PRIMARY KEY (index_col_name,...) + or KEY [index_name] (index_col_name,...) + or INDEX [index_name] (index_col_name,...) + or UNIQUE [INDEX] [index_name] (index_col_name,...) + or FULLTEXT [INDEX] [index_name] (index_col_name,...) + or [CONSTRAINT symbol] FOREIGN KEY [index_name] (index_col_name,...) + [reference_definition] + or CHECK (expr) + */ + + /* + CREATE [UNIQUE|FULLTEXT] INDEX index_name + ON tbl_name (col_name[(length)],... ) + */ + + function _IndexSQL($idxname, $tabname, $flds, $idxoptions) + { + $sql = array(); + + if ( isset($idxoptions['REPLACE']) || isset($idxoptions['DROP']) ) { + if ($this->alterTableAddIndex) $sql[] = "ALTER TABLE $tabname DROP INDEX $idxname"; + else $sql[] = sprintf($this->dropIndex, $idxname, $tabname); + + if ( isset($idxoptions['DROP']) ) + return $sql; + } + + if ( empty ($flds) ) { + return $sql; + } + + if (isset($idxoptions['FULLTEXT'])) { + $unique = ' FULLTEXT'; + } elseif (isset($idxoptions['UNIQUE'])) { + $unique = ' UNIQUE'; + } else { + $unique = ''; + } + + if ( is_array($flds) ) $flds = implode(', ',$flds); + + if ($this->alterTableAddIndex) $s = "ALTER TABLE $tabname ADD $unique INDEX $idxname "; + else $s = 'CREATE' . $unique . ' INDEX ' . $idxname . ' ON ' . $tabname; + + $s .= ' (' . $flds . ')'; + + if ( isset($idxoptions[$this->upperName]) ) + $s .= $idxoptions[$this->upperName]; + + $sql[] = $s; + + return $sql; + } +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-oci8.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-oci8.inc.php new file mode 100644 index 000000000..57cf0af52 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-oci8.inc.php @@ -0,0 +1,300 @@ +type; + $len = $fieldobj->max_length; + } + switch (strtoupper($t)) { + case 'VARCHAR': + case 'VARCHAR2': + case 'CHAR': + case 'VARBINARY': + case 'BINARY': + if (isset($this) && $len <= $this->blobSize) return 'C'; + return 'X'; + + case 'NCHAR': + case 'NVARCHAR2': + case 'NVARCHAR': + if (isset($this) && $len <= $this->blobSize) return 'C2'; + return 'X2'; + + case 'NCLOB': + case 'CLOB': + return 'XL'; + + case 'LONG RAW': + case 'LONG VARBINARY': + case 'BLOB': + return 'B'; + + case 'TIMESTAMP': + return 'TS'; + + case 'DATE': + return 'T'; + + case 'INT': + case 'SMALLINT': + case 'INTEGER': + return 'I'; + + default: + return 'N'; + } + } + + function ActualType($meta) + { + switch($meta) { + case 'C': return 'VARCHAR'; + case 'X': return $this->typeX; + case 'XL': return $this->typeXL; + + case 'C2': return 'NVARCHAR2'; + case 'X2': return 'NVARCHAR2(4000)'; + + case 'B': return 'BLOB'; + + case 'TS': + return 'TIMESTAMP'; + + case 'D': + case 'T': return 'DATE'; + case 'L': return 'NUMBER(1)'; + case 'I1': return 'NUMBER(3)'; + case 'I2': return 'NUMBER(5)'; + case 'I': + case 'I4': return 'NUMBER(10)'; + + case 'I8': return 'NUMBER(20)'; + case 'F': return 'NUMBER'; + case 'N': return 'NUMBER'; + case 'R': return 'NUMBER(20)'; + default: + return $meta; + } + } + + function CreateDatabase($dbname, $options=false) + { + $options = $this->_Options($options); + $password = isset($options['PASSWORD']) ? $options['PASSWORD'] : 'tiger'; + $tablespace = isset($options["TABLESPACE"]) ? " DEFAULT TABLESPACE ".$options["TABLESPACE"] : ''; + $sql[] = "CREATE USER ".$dbname." IDENTIFIED BY ".$password.$tablespace; + $sql[] = "GRANT CREATE SESSION, CREATE TABLE,UNLIMITED TABLESPACE,CREATE SEQUENCE TO $dbname"; + + return $sql; + } + + function AddColumnSQL($tabname, $flds) + { + $tabname = $this->TableName($tabname); + $f = array(); + list($lines,$pkey) = $this->_GenFields($flds); + $s = "ALTER TABLE $tabname ADD ("; + foreach($lines as $v) { + $f[] = "\n $v"; + } + + $s .= implode(', ',$f).')'; + $sql[] = $s; + return $sql; + } + + function AlterColumnSQL($tabname, $flds, $tableflds='', $tableoptions='') + { + $tabname = $this->TableName($tabname); + $f = array(); + list($lines,$pkey) = $this->_GenFields($flds); + $s = "ALTER TABLE $tabname MODIFY("; + foreach($lines as $v) { + $f[] = "\n $v"; + } + $s .= implode(', ',$f).')'; + $sql[] = $s; + return $sql; + } + + function DropColumnSQL($tabname, $flds, $tableflds='', $tableoptions='') + { + if (!is_array($flds)) $flds = explode(',',$flds); + foreach ($flds as $k => $v) $flds[$k] = $this->NameQuote($v); + + $sql = array(); + $s = "ALTER TABLE $tabname DROP("; + $s .= implode(', ',$flds).') CASCADE CONSTRAINTS'; + $sql[] = $s; + return $sql; + } + + function _DropAutoIncrement($t) + { + if (strpos($t,'.') !== false) { + $tarr = explode('.',$t); + return "drop sequence ".$tarr[0].".seq_".$tarr[1]; + } + return "drop sequence seq_".$t; + } + + // return string must begin with space + function _CreateSuffix($fname,&$ftype,$fnotnull,$fdefault,$fautoinc,$fconstraint,$funsigned) + { + $suffix = ''; + + if ($fdefault == "''" && $fnotnull) {// this is null in oracle + $fnotnull = false; + if ($this->debug) ADOConnection::outp("NOT NULL and DEFAULT='' illegal in Oracle"); + } + + if (strlen($fdefault)) $suffix .= " DEFAULT $fdefault"; + if ($fnotnull) $suffix .= ' NOT NULL'; + + if ($fautoinc) $this->seqField = $fname; + if ($fconstraint) $suffix .= ' '.$fconstraint; + + return $suffix; + } + +/* +CREATE or replace TRIGGER jaddress_insert +before insert on jaddress +for each row +begin +select seqaddress.nextval into :new.A_ID from dual; +end; +*/ + function _Triggers($tabname,$tableoptions) + { + if (!$this->seqField) return array(); + + if ($this->schema) { + $t = strpos($tabname,'.'); + if ($t !== false) $tab = substr($tabname,$t+1); + else $tab = $tabname; + $seqname = $this->schema.'.'.$this->seqPrefix.$tab; + $trigname = $this->schema.'.'.$this->trigPrefix.$this->seqPrefix.$tab; + } else { + $seqname = $this->seqPrefix.$tabname; + $trigname = $this->trigPrefix.$seqname; + } + + if (strlen($seqname) > 30) { + $seqname = $this->seqPrefix.uniqid(''); + } // end if + if (strlen($trigname) > 30) { + $trigname = $this->trigPrefix.uniqid(''); + } // end if + + if (isset($tableoptions['REPLACE'])) $sql[] = "DROP SEQUENCE $seqname"; + $seqCache = ''; + if (isset($tableoptions['SEQUENCE_CACHE'])){$seqCache = $tableoptions['SEQUENCE_CACHE'];} + $seqIncr = ''; + if (isset($tableoptions['SEQUENCE_INCREMENT'])){$seqIncr = ' INCREMENT BY '.$tableoptions['SEQUENCE_INCREMENT'];} + $seqStart = ''; + if (isset($tableoptions['SEQUENCE_START'])){$seqIncr = ' START WITH '.$tableoptions['SEQUENCE_START'];} + $sql[] = "CREATE SEQUENCE $seqname $seqStart $seqIncr $seqCache"; + $sql[] = "CREATE OR REPLACE TRIGGER $trigname BEFORE insert ON $tabname FOR EACH ROW WHEN (NEW.$this->seqField IS NULL OR NEW.$this->seqField = 0) BEGIN select $seqname.nextval into :new.$this->seqField from dual; END;"; + + $this->seqField = false; + return $sql; + } + + /* + CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name [(create_definition,...)] + [table_options] [select_statement] + create_definition: + col_name type [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] + [PRIMARY KEY] [reference_definition] + or PRIMARY KEY (index_col_name,...) + or KEY [index_name] (index_col_name,...) + or INDEX [index_name] (index_col_name,...) + or UNIQUE [INDEX] [index_name] (index_col_name,...) + or FULLTEXT [INDEX] [index_name] (index_col_name,...) + or [CONSTRAINT symbol] FOREIGN KEY [index_name] (index_col_name,...) + [reference_definition] + or CHECK (expr) + */ + + + + function _IndexSQL($idxname, $tabname, $flds,$idxoptions) + { + $sql = array(); + + if ( isset($idxoptions['REPLACE']) || isset($idxoptions['DROP']) ) { + $sql[] = sprintf ($this->dropIndex, $idxname, $tabname); + if ( isset($idxoptions['DROP']) ) + return $sql; + } + + if ( empty ($flds) ) { + return $sql; + } + + if (isset($idxoptions['BITMAP'])) { + $unique = ' BITMAP'; + } elseif (isset($idxoptions['UNIQUE'])) { + $unique = ' UNIQUE'; + } else { + $unique = ''; + } + + if ( is_array($flds) ) + $flds = implode(', ',$flds); + $s = 'CREATE' . $unique . ' INDEX ' . $idxname . ' ON ' . $tabname . ' (' . $flds . ')'; + + if ( isset($idxoptions[$this->upperName]) ) + $s .= $idxoptions[$this->upperName]; + + if (isset($idxoptions['oci8'])) + $s .= $idxoptions['oci8']; + + + $sql[] = $s; + + return $sql; + } + + function GetCommentSQL($table,$col) + { + $table = $this->connection->qstr($table); + $col = $this->connection->qstr($col); + return "select comments from USER_COL_COMMENTS where TABLE_NAME=$table and COLUMN_NAME=$col"; + } + + function SetCommentSQL($table,$col,$cmt) + { + $cmt = $this->connection->qstr($cmt); + return "COMMENT ON COLUMN $table.$col IS $cmt"; + } +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-postgres.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-postgres.inc.php new file mode 100644 index 000000000..99a564131 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-postgres.inc.php @@ -0,0 +1,484 @@ +type; + $len = $fieldobj->max_length; + } + $is_serial = is_object($fieldobj) && !empty($fieldobj->primary_key) && !empty($fieldobj->unique) && + !empty($fieldobj->has_default) && substr($fieldobj->default_value,0,8) == 'nextval('; + + switch (strtoupper($t)) { + case 'INTERVAL': + case 'CHAR': + case 'CHARACTER': + case 'VARCHAR': + case 'NAME': + case 'BPCHAR': + if ($len <= $this->blobSize) return 'C'; + + case 'TEXT': + return 'X'; + + case 'IMAGE': // user defined type + case 'BLOB': // user defined type + case 'BIT': // This is a bit string, not a single bit, so don't return 'L' + case 'VARBIT': + case 'BYTEA': + return 'B'; + + case 'BOOL': + case 'BOOLEAN': + return 'L'; + + case 'DATE': + return 'D'; + + case 'TIME': + case 'DATETIME': + case 'TIMESTAMP': + case 'TIMESTAMPTZ': + return 'T'; + + case 'INTEGER': return !$is_serial ? 'I' : 'R'; + case 'SMALLINT': + case 'INT2': return !$is_serial ? 'I2' : 'R'; + case 'INT4': return !$is_serial ? 'I4' : 'R'; + case 'BIGINT': + case 'INT8': return !$is_serial ? 'I8' : 'R'; + + case 'OID': + case 'SERIAL': + return 'R'; + + case 'FLOAT4': + case 'FLOAT8': + case 'DOUBLE PRECISION': + case 'REAL': + return 'F'; + + default: + return 'N'; + } + } + + function ActualType($meta) + { + switch($meta) { + case 'C': return 'VARCHAR'; + case 'XL': + case 'X': return 'TEXT'; + + case 'C2': return 'VARCHAR'; + case 'X2': return 'TEXT'; + + case 'B': return 'BYTEA'; + + case 'D': return 'DATE'; + case 'TS': + case 'T': return 'TIMESTAMP'; + + case 'L': return 'BOOLEAN'; + case 'I': return 'INTEGER'; + case 'I1': return 'SMALLINT'; + case 'I2': return 'INT2'; + case 'I4': return 'INT4'; + case 'I8': return 'INT8'; + + case 'F': return 'FLOAT8'; + case 'N': return 'NUMERIC'; + default: + return $meta; + } + } + + /** + * Adding a new Column + * + * reimplementation of the default function as postgres does NOT allow to set the default in the same statement + * + * @param string $tabname table-name + * @param string $flds column-names and types for the changed columns + * @return array with SQL strings + */ + function AddColumnSQL($tabname, $flds) + { + $tabname = $this->TableName ($tabname); + $sql = array(); + $not_null = false; + list($lines,$pkey) = $this->_GenFields($flds); + $alter = 'ALTER TABLE ' . $tabname . $this->addCol . ' '; + foreach($lines as $v) { + if (($not_null = preg_match('/NOT NULL/i',$v))) { + $v = preg_replace('/NOT NULL/i','',$v); + } + if (preg_match('/^([^ ]+) .*DEFAULT (\'[^\']+\'|\"[^\"]+\"|[^ ]+)/',$v,$matches)) { + list(,$colname,$default) = $matches; + $sql[] = $alter . str_replace('DEFAULT '.$default,'',$v); + $sql[] = 'UPDATE '.$tabname.' SET '.$colname.'='.$default; + $sql[] = 'ALTER TABLE '.$tabname.' ALTER COLUMN '.$colname.' SET DEFAULT ' . $default; + } else { + $sql[] = $alter . $v; + } + if ($not_null) { + list($colname) = explode(' ',$v); + $sql[] = 'ALTER TABLE '.$tabname.' ALTER COLUMN '.$colname.' SET NOT NULL'; + } + } + return $sql; + } + + + function DropIndexSQL ($idxname, $tabname = NULL) + { + return array(sprintf($this->dropIndex, $this->TableName($idxname), $this->TableName($tabname))); + } + + /** + * Change the definition of one column + * + * Postgres can't do that on it's own, you need to supply the complete defintion of the new table, + * to allow, recreating the table and copying the content over to the new table + * @param string $tabname table-name + * @param string $flds column-name and type for the changed column + * @param string $tableflds complete defintion of the new table, eg. for postgres, default '' + * @param array/ $tableoptions options for the new table see CreateTableSQL, default '' + * @return array with SQL strings + */ + /* + function AlterColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + if (!$tableflds) { + if ($this->debug) ADOConnection::outp("AlterColumnSQL needs a complete table-definiton for PostgreSQL"); + return array(); + } + return $this->_recreate_copy_table($tabname,False,$tableflds,$tableoptions); + }*/ + + function AlterColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + // Check if alter single column datatype available - works with 8.0+ + $has_alter_column = 8.0 <= (float) @$this->serverInfo['version']; + + if ($has_alter_column) { + $tabname = $this->TableName($tabname); + $sql = array(); + list($lines,$pkey) = $this->_GenFields($flds); + $set_null = false; + foreach($lines as $v) { + $alter = 'ALTER TABLE ' . $tabname . $this->alterCol . ' '; + if ($not_null = preg_match('/NOT NULL/i',$v)) { + $v = preg_replace('/NOT NULL/i','',$v); + } + // this next block doesn't work - there is no way that I can see to + // explicitly ask a column to be null using $flds + else if ($set_null = preg_match('/NULL/i',$v)) { + // if they didn't specify not null, see if they explicitely asked for null + // Lookbehind pattern covers the case 'fieldname NULL datatype DEFAULT NULL' + // only the first NULL should be removed, not the one specifying + // the default value + $v = preg_replace('/(?MetaColumns($tabname); + list(,$colname,$default) = $matches; + $alter .= $colname; + if ($this->connection) { + $old_coltype = $this->connection->MetaType($existing[strtoupper($colname)]); + } + else { + $old_coltype = $t; + } + $v = preg_replace('/^' . preg_quote($colname) . '\s/', '', $v); + $t = trim(str_replace('DEFAULT '.$default,'',$v)); + + // Type change from bool to int + if ( $old_coltype == 'L' && $t == 'INTEGER' ) { + $sql[] = $alter . ' DROP DEFAULT'; + $sql[] = $alter . " TYPE $t USING ($colname::BOOL)::INT"; + $sql[] = $alter . " SET DEFAULT $default"; + } + // Type change from int to bool + else if ( $old_coltype == 'I' && $t == 'BOOLEAN' ) { + if( strcasecmp('NULL', trim($default)) != 0 ) { + $default = $this->connection->qstr($default); + } + $sql[] = $alter . ' DROP DEFAULT'; + $sql[] = $alter . " TYPE $t USING CASE WHEN $colname = 0 THEN false ELSE true END"; + $sql[] = $alter . " SET DEFAULT $default"; + } + // Any other column types conversion + else { + $sql[] = $alter . " TYPE $t"; + $sql[] = $alter . " SET DEFAULT $default"; + } + + } + else { + // drop default? + preg_match ('/^\s*(\S+)\s+(.*)$/',$v,$matches); + list (,$colname,$rest) = $matches; + $alter .= $colname; + $sql[] = $alter . ' TYPE ' . $rest; + } + +# list($colname) = explode(' ',$v); + if ($not_null) { + // this does not error out if the column is already not null + $sql[] = $alter . ' SET NOT NULL'; + } + if ($set_null) { + // this does not error out if the column is already null + $sql[] = $alter . ' DROP NOT NULL'; + } + } + return $sql; + } + + // does not have alter column + if (!$tableflds) { + if ($this->debug) ADOConnection::outp("AlterColumnSQL needs a complete table-definiton for PostgreSQL"); + return array(); + } + return $this->_recreate_copy_table($tabname,False,$tableflds,$tableoptions); + } + + /** + * Drop one column + * + * Postgres < 7.3 can't do that on it's own, you need to supply the complete defintion of the new table, + * to allow, recreating the table and copying the content over to the new table + * @param string $tabname table-name + * @param string $flds column-name and type for the changed column + * @param string $tableflds complete defintion of the new table, eg. for postgres, default '' + * @param array/ $tableoptions options for the new table see CreateTableSQL, default '' + * @return array with SQL strings + */ + function DropColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + $has_drop_column = 7.3 <= (float) @$this->serverInfo['version']; + if (!$has_drop_column && !$tableflds) { + if ($this->debug) ADOConnection::outp("DropColumnSQL needs complete table-definiton for PostgreSQL < 7.3"); + return array(); + } + if ($has_drop_column) { + return ADODB_DataDict::DropColumnSQL($tabname, $flds); + } + return $this->_recreate_copy_table($tabname,$flds,$tableflds,$tableoptions); + } + + /** + * Save the content into a temp. table, drop and recreate the original table and copy the content back in + * + * We also take care to set the values of the sequenz and recreate the indexes. + * All this is done in a transaction, to not loose the content of the table, if something went wrong! + * @internal + * @param string $tabname table-name + * @param string $dropflds column-names to drop + * @param string $tableflds complete defintion of the new table, eg. for postgres + * @param array/string $tableoptions options for the new table see CreateTableSQL, default '' + * @return array with SQL strings + */ + function _recreate_copy_table($tabname,$dropflds,$tableflds,$tableoptions='') + { + if ($dropflds && !is_array($dropflds)) $dropflds = explode(',',$dropflds); + $copyflds = array(); + foreach($this->MetaColumns($tabname) as $fld) { + if (!$dropflds || !in_array($fld->name,$dropflds)) { + // we need to explicit convert varchar to a number to be able to do an AlterColumn of a char column to a nummeric one + if (preg_match('/'.$fld->name.' (I|I2|I4|I8|N|F)/i',$tableflds,$matches) && + in_array($fld->type,array('varchar','char','text','bytea'))) { + $copyflds[] = "to_number($fld->name,'S9999999999999D99')"; + } else { + $copyflds[] = $fld->name; + } + // identify the sequence name and the fld its on + if ($fld->primary_key && $fld->has_default && + preg_match("/nextval\('([^']+)'::text\)/",$fld->default_value,$matches)) { + $seq_name = $matches[1]; + $seq_fld = $fld->name; + } + } + } + $copyflds = implode(', ',$copyflds); + + $tempname = $tabname.'_tmp'; + $aSql[] = 'BEGIN'; // we use a transaction, to make sure not to loose the content of the table + $aSql[] = "SELECT * INTO TEMPORARY TABLE $tempname FROM $tabname"; + $aSql = array_merge($aSql,$this->DropTableSQL($tabname)); + $aSql = array_merge($aSql,$this->CreateTableSQL($tabname,$tableflds,$tableoptions)); + $aSql[] = "INSERT INTO $tabname SELECT $copyflds FROM $tempname"; + if ($seq_name && $seq_fld) { // if we have a sequence we need to set it again + $seq_name = $tabname.'_'.$seq_fld.'_seq'; // has to be the name of the new implicit sequence + $aSql[] = "SELECT setval('$seq_name',MAX($seq_fld)) FROM $tabname"; + } + $aSql[] = "DROP TABLE $tempname"; + // recreate the indexes, if they not contain one of the droped columns + foreach($this->MetaIndexes($tabname) as $idx_name => $idx_data) + { + if (substr($idx_name,-5) != '_pkey' && (!$dropflds || !count(array_intersect($dropflds,$idx_data['columns'])))) { + $aSql = array_merge($aSql,$this->CreateIndexSQL($idx_name,$tabname,$idx_data['columns'], + $idx_data['unique'] ? array('UNIQUE') : False)); + } + } + $aSql[] = 'COMMIT'; + return $aSql; + } + + function DropTableSQL($tabname) + { + $sql = ADODB_DataDict::DropTableSQL($tabname); + + $drop_seq = $this->_DropAutoIncrement($tabname); + if ($drop_seq) $sql[] = $drop_seq; + + return $sql; + } + + // return string must begin with space + function _CreateSuffix($fname, &$ftype, $fnotnull,$fdefault,$fautoinc,$fconstraint,$funsigned) + { + if ($fautoinc) { + $ftype = 'SERIAL'; + return ''; + } + $suffix = ''; + if (strlen($fdefault)) $suffix .= " DEFAULT $fdefault"; + if ($fnotnull) $suffix .= ' NOT NULL'; + if ($fconstraint) $suffix .= ' '.$fconstraint; + return $suffix; + } + + // search for a sequece for the given table (asumes the seqence-name contains the table-name!) + // if yes return sql to drop it + // this is still necessary if postgres < 7.3 or the SERIAL was created on an earlier version!!! + function _DropAutoIncrement($tabname) + { + $tabname = $this->connection->quote('%'.$tabname.'%'); + + $seq = $this->connection->GetOne("SELECT relname FROM pg_class WHERE NOT relname ~ 'pg_.*' AND relname LIKE $tabname AND relkind='S'"); + + // check if a tables depends on the sequenz and it therefor cant and dont need to be droped separatly + if (!$seq || $this->connection->GetOne("SELECT relname FROM pg_class JOIN pg_depend ON pg_class.relfilenode=pg_depend.objid WHERE relname='$seq' AND relkind='S' AND deptype='i'")) { + return False; + } + return "DROP SEQUENCE ".$seq; + } + + function RenameTableSQL($tabname,$newname) + { + if (!empty($this->schema)) { + $rename_from = $this->TableName($tabname); + $schema_save = $this->schema; + $this->schema = false; + $rename_to = $this->TableName($newname); + $this->schema = $schema_save; + return array (sprintf($this->renameTable, $rename_from, $rename_to)); + } + + return array (sprintf($this->renameTable, $this->TableName($tabname),$this->TableName($newname))); + } + + /* + CREATE [ [ LOCAL ] { TEMPORARY | TEMP } ] TABLE table_name ( + { column_name data_type [ DEFAULT default_expr ] [ column_constraint [, ... ] ] + | table_constraint } [, ... ] + ) + [ INHERITS ( parent_table [, ... ] ) ] + [ WITH OIDS | WITHOUT OIDS ] + where column_constraint is: + [ CONSTRAINT constraint_name ] + { NOT NULL | NULL | UNIQUE | PRIMARY KEY | + CHECK (expression) | + REFERENCES reftable [ ( refcolumn ) ] [ MATCH FULL | MATCH PARTIAL ] + [ ON DELETE action ] [ ON UPDATE action ] } + [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] + and table_constraint is: + [ CONSTRAINT constraint_name ] + { UNIQUE ( column_name [, ... ] ) | + PRIMARY KEY ( column_name [, ... ] ) | + CHECK ( expression ) | + FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ] + [ MATCH FULL | MATCH PARTIAL ] [ ON DELETE action ] [ ON UPDATE action ] } + [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] + */ + + + /* + CREATE [ UNIQUE ] INDEX index_name ON table +[ USING acc_method ] ( column [ ops_name ] [, ...] ) +[ WHERE predicate ] +CREATE [ UNIQUE ] INDEX index_name ON table +[ USING acc_method ] ( func_name( column [, ... ]) [ ops_name ] ) +[ WHERE predicate ] + */ + function _IndexSQL($idxname, $tabname, $flds, $idxoptions) + { + $sql = array(); + + if ( isset($idxoptions['REPLACE']) || isset($idxoptions['DROP']) ) { + $sql[] = sprintf ($this->dropIndex, $idxname, $tabname); + if ( isset($idxoptions['DROP']) ) + return $sql; + } + + if ( empty ($flds) ) { + return $sql; + } + + $unique = isset($idxoptions['UNIQUE']) ? ' UNIQUE' : ''; + + $s = 'CREATE' . $unique . ' INDEX ' . $idxname . ' ON ' . $tabname . ' '; + + if (isset($idxoptions['HASH'])) + $s .= 'USING HASH '; + + if ( isset($idxoptions[$this->upperName]) ) + $s .= $idxoptions[$this->upperName]; + + if ( is_array($flds) ) + $flds = implode(', ',$flds); + $s .= '(' . $flds . ')'; + $sql[] = $s; + + return $sql; + } + + function _GetSize($ftype, $ty, $fsize, $fprec) + { + if (strlen($fsize) && $ty != 'X' && $ty != 'B' && $ty != 'I' && strpos($ftype,'(') === false) { + $ftype .= "(".$fsize; + if (strlen($fprec)) $ftype .= ",".$fprec; + $ftype .= ')'; + } + return $ftype; + } +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-sapdb.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-sapdb.inc.php new file mode 100644 index 000000000..fbf931c73 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-sapdb.inc.php @@ -0,0 +1,122 @@ +type; + $len = $fieldobj->max_length; + } + static $maxdb_type2adodb = array( + 'VARCHAR' => 'C', + 'CHARACTER' => 'C', + 'LONG' => 'X', // no way to differ between 'X' and 'B' :-( + 'DATE' => 'D', + 'TIMESTAMP' => 'T', + 'BOOLEAN' => 'L', + 'INTEGER' => 'I4', + 'SMALLINT' => 'I2', + 'FLOAT' => 'F', + 'FIXED' => 'N', + ); + $type = isset($maxdb_type2adodb[$t]) ? $maxdb_type2adodb[$t] : 'C'; + + // convert integer-types simulated with fixed back to integer + if ($t == 'FIXED' && !$fieldobj->scale && ($len == 20 || $len == 3)) { + $type = $len == 20 ? 'I8' : 'I1'; + } + if ($fieldobj->auto_increment) $type = 'R'; + + return $type; + } + + // return string must begin with space + function _CreateSuffix($fname,&$ftype,$fnotnull,$fdefault,$fautoinc,$fconstraint,$funsigned) + { + $suffix = ''; + if ($funsigned) $suffix .= ' UNSIGNED'; + if ($fnotnull) $suffix .= ' NOT NULL'; + if ($fautoinc) $suffix .= ' DEFAULT SERIAL'; + elseif (strlen($fdefault)) $suffix .= " DEFAULT $fdefault"; + if ($fconstraint) $suffix .= ' '.$fconstraint; + return $suffix; + } + + function AddColumnSQL($tabname, $flds) + { + $tabname = $this->TableName ($tabname); + $sql = array(); + list($lines,$pkey) = $this->_GenFields($flds); + return array( 'ALTER TABLE ' . $tabname . ' ADD (' . implode(', ',$lines) . ')' ); + } + + function AlterColumnSQL($tabname, $flds, $tableflds='', $tableoptions='') + { + $tabname = $this->TableName ($tabname); + $sql = array(); + list($lines,$pkey) = $this->_GenFields($flds); + return array( 'ALTER TABLE ' . $tabname . ' MODIFY (' . implode(', ',$lines) . ')' ); + } + + function DropColumnSQL($tabname, $flds, $tableflds='',$tableoptions='') + { + $tabname = $this->TableName ($tabname); + if (!is_array($flds)) $flds = explode(',',$flds); + foreach($flds as $k => $v) { + $flds[$k] = $this->NameQuote($v); + } + return array( 'ALTER TABLE ' . $tabname . ' DROP (' . implode(', ',$flds) . ')' ); + } +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-sqlite.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-sqlite.inc.php new file mode 100644 index 000000000..86b1b0478 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-sqlite.inc.php @@ -0,0 +1,90 @@ +debug) ADOConnection::outp("AlterColumnSQL not supported natively by SQLite"); + return array(); + } + + function DropColumnSQL($tabname, $flds, $tableflds='', $tableoptions='') + { + if ($this->debug) ADOConnection::outp("DropColumnSQL not supported natively by SQLite"); + return array(); + } + + function RenameColumnSQL($tabname,$oldcolumn,$newcolumn,$flds='') + { + if ($this->debug) ADOConnection::outp("RenameColumnSQL not supported natively by SQLite"); + return array(); + } + +} diff --git a/app/vendor/adodb/adodb-php/datadict/datadict-sybase.inc.php b/app/vendor/adodb/adodb-php/datadict/datadict-sybase.inc.php new file mode 100644 index 000000000..d4e5f0530 --- /dev/null +++ b/app/vendor/adodb/adodb-php/datadict/datadict-sybase.inc.php @@ -0,0 +1,230 @@ +type; + $len = $fieldobj->max_length; + } + + $len = -1; // mysql max_length is not accurate + switch (strtoupper($t)) { + + case 'INT': + case 'INTEGER': return 'I'; + case 'BIT': + case 'TINYINT': return 'I1'; + case 'SMALLINT': return 'I2'; + case 'BIGINT': return 'I8'; + + case 'REAL': + case 'FLOAT': return 'F'; + default: return parent::MetaType($t,$len,$fieldobj); + } + } + + function ActualType($meta) + { + switch(strtoupper($meta)) { + case 'C': return 'VARCHAR'; + case 'XL': + case 'X': return 'TEXT'; + + case 'C2': return 'NVARCHAR'; + case 'X2': return 'NTEXT'; + + case 'B': return 'IMAGE'; + + case 'D': return 'DATETIME'; + case 'TS': + case 'T': return 'DATETIME'; + case 'L': return 'BIT'; + + case 'I': return 'INT'; + case 'I1': return 'TINYINT'; + case 'I2': return 'SMALLINT'; + case 'I4': return 'INT'; + case 'I8': return 'BIGINT'; + + case 'F': return 'REAL'; + case 'N': return 'NUMERIC'; + default: + return $meta; + } + } + + + function AddColumnSQL($tabname, $flds) + { + $tabname = $this->TableName ($tabname); + $f = array(); + list($lines,$pkey) = $this->_GenFields($flds); + $s = "ALTER TABLE $tabname $this->addCol"; + foreach($lines as $v) { + $f[] = "\n $v"; + } + $s .= implode(', ',$f); + $sql[] = $s; + return $sql; + } + + function AlterColumnSQL($tabname, $flds, $tableflds='', $tableoptions='') + { + $tabname = $this->TableName ($tabname); + $sql = array(); + list($lines,$pkey) = $this->_GenFields($flds); + foreach($lines as $v) { + $sql[] = "ALTER TABLE $tabname $this->alterCol $v"; + } + + return $sql; + } + + function DropColumnSQL($tabname, $flds, $tableflds='', $tableoptions='') + { + $tabname = $this->TableName($tabname); + if (!is_array($flds)) $flds = explode(',',$flds); + $f = array(); + $s = "ALTER TABLE $tabname"; + foreach($flds as $v) { + $f[] = "\n$this->dropCol ".$this->NameQuote($v); + } + $s .= implode(', ',$f); + $sql[] = $s; + return $sql; + } + + // return string must begin with space + function _CreateSuffix($fname,&$ftype,$fnotnull,$fdefault,$fautoinc,$fconstraint,$funsigned) + { + $suffix = ''; + if (strlen($fdefault)) $suffix .= " DEFAULT $fdefault"; + if ($fautoinc) $suffix .= ' DEFAULT AUTOINCREMENT'; + if ($fnotnull) $suffix .= ' NOT NULL'; + else if ($suffix == '') $suffix .= ' NULL'; + if ($fconstraint) $suffix .= ' '.$fconstraint; + return $suffix; + } + + /* +CREATE TABLE + [ database_name.[ owner ] . | owner. ] table_name + ( { < column_definition > + | column_name AS computed_column_expression + | < table_constraint > ::= [ CONSTRAINT constraint_name ] } + + | [ { PRIMARY KEY | UNIQUE } [ ,...n ] + ) + +[ ON { filegroup | DEFAULT } ] +[ TEXTIMAGE_ON { filegroup | DEFAULT } ] + +< column_definition > ::= { column_name data_type } + [ COLLATE < collation_name > ] + [ [ DEFAULT constant_expression ] + | [ IDENTITY [ ( seed , increment ) [ NOT FOR REPLICATION ] ] ] + ] + [ ROWGUIDCOL] + [ < column_constraint > ] [ ...n ] + +< column_constraint > ::= [ CONSTRAINT constraint_name ] + { [ NULL | NOT NULL ] + | [ { PRIMARY KEY | UNIQUE } + [ CLUSTERED | NONCLUSTERED ] + [ WITH FILLFACTOR = fillfactor ] + [ON {filegroup | DEFAULT} ] ] + ] + | [ [ FOREIGN KEY ] + REFERENCES ref_table [ ( ref_column ) ] + [ ON DELETE { CASCADE | NO ACTION } ] + [ ON UPDATE { CASCADE | NO ACTION } ] + [ NOT FOR REPLICATION ] + ] + | CHECK [ NOT FOR REPLICATION ] + ( logical_expression ) + } + +< table_constraint > ::= [ CONSTRAINT constraint_name ] + { [ { PRIMARY KEY | UNIQUE } + [ CLUSTERED | NONCLUSTERED ] + { ( column [ ASC | DESC ] [ ,...n ] ) } + [ WITH FILLFACTOR = fillfactor ] + [ ON { filegroup | DEFAULT } ] + ] + | FOREIGN KEY + [ ( column [ ,...n ] ) ] + REFERENCES ref_table [ ( ref_column [ ,...n ] ) ] + [ ON DELETE { CASCADE | NO ACTION } ] + [ ON UPDATE { CASCADE | NO ACTION } ] + [ NOT FOR REPLICATION ] + | CHECK [ NOT FOR REPLICATION ] + ( search_conditions ) + } + + + */ + + /* + CREATE [ UNIQUE ] [ CLUSTERED | NONCLUSTERED ] INDEX index_name + ON { table | view } ( column [ ASC | DESC ] [ ,...n ] ) + [ WITH < index_option > [ ,...n] ] + [ ON filegroup ] + < index_option > :: = + { PAD_INDEX | + FILLFACTOR = fillfactor | + IGNORE_DUP_KEY | + DROP_EXISTING | + STATISTICS_NORECOMPUTE | + SORT_IN_TEMPDB + } +*/ + function _IndexSQL($idxname, $tabname, $flds, $idxoptions) + { + $sql = array(); + + if ( isset($idxoptions['REPLACE']) || isset($idxoptions['DROP']) ) { + $sql[] = sprintf ($this->dropIndex, $idxname, $tabname); + if ( isset($idxoptions['DROP']) ) + return $sql; + } + + if ( empty ($flds) ) { + return $sql; + } + + $unique = isset($idxoptions['UNIQUE']) ? ' UNIQUE' : ''; + $clustered = isset($idxoptions['CLUSTERED']) ? ' CLUSTERED' : ''; + + if ( is_array($flds) ) + $flds = implode(', ',$flds); + $s = 'CREATE' . $unique . $clustered . ' INDEX ' . $idxname . ' ON ' . $tabname . ' (' . $flds . ')'; + + if ( isset($idxoptions[$this->upperName]) ) + $s .= $idxoptions[$this->upperName]; + + $sql[] = $s; + + return $sql; + } +} diff --git a/app/vendor/adodb/adodb-php/docs/README.md b/app/vendor/adodb/adodb-php/docs/README.md new file mode 100644 index 000000000..b0ec29417 --- /dev/null +++ b/app/vendor/adodb/adodb-php/docs/README.md @@ -0,0 +1,17 @@ +# ADOdb Documentation + +ADOdb documentation is available in the following locations + +- [Online](http://adodb.org/) +- [Download](https://sourceforge.net/projects/adodb/files/Documentation/) for offline use + +## Legacy documentation + +The old HTML files are available in +[GitHub](https://github.com/ADOdb/ADOdb/tree/8b8133771ecbe9c95e57abbe5dc3757f0226bfcd/docs), +or in the release zip/tarballs for version 5.20 and before on +[Sourceforge](https://sourceforge.net/projects/adodb/files/adodb-php5-only/). + +## Changelog + +The full historical [Changelog](changelog.md) is available on GitHub. diff --git a/app/vendor/adodb/adodb-php/docs/adodb.gif b/app/vendor/adodb/adodb-php/docs/adodb.gif new file mode 100644 index 000000000..c5e8dfc6d Binary files /dev/null and b/app/vendor/adodb/adodb-php/docs/adodb.gif differ diff --git a/app/vendor/adodb/adodb-php/docs/adodb2.gif b/app/vendor/adodb/adodb-php/docs/adodb2.gif new file mode 100644 index 000000000..f12ae2037 Binary files /dev/null and b/app/vendor/adodb/adodb-php/docs/adodb2.gif differ diff --git a/app/vendor/adodb/adodb-php/docs/changelog.md b/app/vendor/adodb/adodb-php/docs/changelog.md new file mode 100644 index 000000000..1e097e962 --- /dev/null +++ b/app/vendor/adodb/adodb-php/docs/changelog.md @@ -0,0 +1,491 @@ +# ADOdb Changelog - v5.x + +Older changelogs: +[v4.x](changelog_v4.x.md), +[v3.x](changelog_v3.x.md), +[v2.x](changelog_v2.x.md). + +## 5.20.9 - 21-Dec-2016 + +- mssql: fix syntax error in version matching regex #305 + +## 5.20.8 - 17-Dec-2016 + +- mssql: support MSSQL Server 2016 and later #294 +- mssql: fix Find() returning no results. #298 +- mssql: fix Sequence name forced to 'adodbseq'. #295, #300 +- mssql: fix GenId() not returning next sequence value with SQL Server 2005/2008. #302 +- mssql: fix drop/alter column with existing default constraint. #290 +- mssql: fix PHP notice in MetaColumns(). #289 +- oci8po: fix inconsistent variable binding in SelectLimit() #288 +- oci8po: fix SelectLimit() with prepared statements #282 + +## 5.20.7 - 20-Sep-2016 + +- security: Fix SQL injection in PDO drivers qstr() method (CVE-2016-7405). #226 +- oci8po: prevent segfault on PHP 7. #259 +- pdo/mysql: Fix MetaTables() method. #275 + +## 5.20.6 - 31-Aug-2016 + +- security: Fix XSS vulnerability in old test script (CVE-2016-4855). #274 +- adodb: Exit with error/exception when the ADOdb Extension is loaded. #269 +- adodb: Fix truncated exception messages. #273 + +## 5.20.5 - 10-Aug-2016 + +- adodb: Fix fatal error when connecting with missing extension. #254 +- adodb: Fix _adodb_getcount(). #236 +- mssql: Destructor fails if recordset already closed. #268 +- mssql: Use SQL server native data types if available. #234 +- mysqli: Fix PHP notice in _close() method. #240 +- pdo: Let driver handle SelectDB() and SQLDate() calls. #242 +- xml: Fix PHP strict warning. #260 +- xml: remove calls to 'unset($this)' (PHP 7.1 compatibility). #257 + +## 5.20.4 - 31-Mar-2016 + +- adodb: Fix BulkBind() param count validation. #199 +- mysqli: fix PHP warning in recordset destructor. #217 +- mysqli: cast port number to int when connecting (PHP7 compatibility). #218 + +## 5.20.3 - 01-Jan-2016 + +- mssql: PHP warning when closing recordset from destructor not fixed in v5.20.2. #180 + +## 5.20.2 - 27-Dec-2015 + +- adodb: Remove a couple leftover PHP 4.x constructors (PHP7 compatibility). #139 +- db2ora: Remove deprecated preg_replace '/e' flag (PHP7 compatibility). #168 +- mysql: MoveNext() now respects ADODB_ASSOC_CASE. #167 +- mssql, mysql, informix: Avoid PHP warning when closing recordset from destructor. #170 + +## 5.20.1 - 06-Dec-2015 + +- adodb: Fix regression introduced in 5.20.0, causing a PHP Warning when + calling GetAssoc() on an empty recordset. See Github #162 +- ADOConnection::Version() now handles SemVer. See Github #164 + +## 5.20.0 - 28-Nov-2015 + +- adodb: Fix regression introduced in v5.19, causing queries to return empty rows. See Github #20, #93, #95 +- adodb: Fix regression introduced in v5.19 in GetAssoc() with ADODB_FETCH_ASSOC mode and '0' as data. See Github #102 +- adodb: AutoExecute correctly handles empty result set in case of updates. See Github #13 +- adodb: Fix regex in Version(). See Github #16 +- adodb: Align method signatures to definition in parent class ADODB_DataDict. See Github #31 +- adodb: Improve compatibility of ADORecordSet_empty, thanks to Sjan Evardsson. See Github #43 +- adodb: fix ADODB_Session::open() failing after successful ADONewConnection() call, thanks to Sjan Evardsson. See Github #44 +- adodb: Only include memcache library once for PHPUnit 4.x, thanks to Alan Farquharson. See Github #74 +- adodb: Move() returns false when given row is < 0, thanks to Mike Benoit. +- adodb: Add support for pagination with complex queries, thanks to Mike Benoit. See Github #88 +- adodb: Parse port out of hostname if specified in connection parameters, thanks to Andy Theuninck. See Github #63 +- adodb: Fix inability to set values from 0 to null (and vice versa) with Active Record, thanks to Louis Johnson. See Github #71 +- adodb: Fix PHP strict warning in ADODB_Active_Record::Reload(), thanks to Boštjan Žokš. See Github #75 +- adodb: Add mssql's DATETIME2 type to ADOConnection::MetaType(), thanks to MarcelTO. See Github #80 +- adodb: When flushing cache, initialize it if it is not set, thanks to Paul Haggart. See Github #57 +- adodb: Define DB_AUTOQUERY_* constants in main include file. See Github #49 +- adodb: Improve documentation of fetch mode and assoc case +- adodb: Improve logic to build the assoc case bind array +- adodb: Strict-standards compliance for function names. See Github #18, #142 +- adodb: Remove old PHP 4.x constructors for compatibility with PHP 7. See Github #139 +- adodb: Initialize charset in ADOConnection::SetCharSet. See Github #39 +- adodb: Fix incorrect handling of input array in Execute(). See Github #146 +- adodb: Release Recordset when raising exception. See Github #143 +- adodb: Added new setConnectionParameter() method, currently implemented in mssqlnative driver only. See Github #158. +- adodb-lib: Optimize query pagination, thanks to Mike Benoit. See Github #110 +- memcache: use include_once() to avoid issues with PHPUnit. See http://phplens.com/lens/lensforum/msgs.php?id=19489 +- mssql_n: Allow use of prepared statements with driver. See Github #22 +- mssqlnative: Use ADOConnection::outp instead of error_log. See Github #12 +- mssqlnative: fix failure on Insert_ID() if the insert statement contains a semicolon in a value string, thanks to sketule. See Github #96 +- mssqlnative: Fix "invalid parameter was passed to sqlsrv_configure" error, thanks to Ray Morris. See Github #103 +- mssqlnative: Fix insert_ID() failing if server returns more than 1 row, thanks to gitjti. See Github #41 +- mysql: prevent race conditions when creating/dropping sequences, thanks to MikeB. See Github #28 +- mysql: Fix adodb_strip_order_by() bug causing SQL error for subqueries with order/limit clause, thanks to MikeB. +- mysql: workaround for HHVM behavior, thanks to Mike Benoit. +- mysqli: Fix qstr() when called without an active connection. See Github #11 +- oci8: Fix broken quoting of table name in AddColumnSQL and AlterColumnSQL, thanks to Andreas Fernandez. see Github #67 +- oci8: Allow oci8 driver to use lowercase field names in assoc mode. See Github #21 +- oci8po: Prevent replacement of '?' within strings, thanks to Mark Newnham. See Github #132 +- pdo: Added missing property (fixes PHP notices). see Github #56 +- pdo: Align method signatures with parent class, thanks to Andy Theuninck. see Github #62 +- pdo: new sqlsrv driver, thanks to MarcelTO. See Github #81 +- pdo/mysql: New methods to make the driver behave more like mysql/mysqli, thanks to Andy Theuninck. see Github #40 +- postgres: Stop using legacy function aliases +- postgres: Fix AlterColumnSQL when updating multiple columns, thanks to Jouni Ahto. See Github #72 +- postgres: Fix support for HHVM 3.6, thanks to Mike Benoit. See Github #87 +- postgres: Noblob optimization, thanks to Mike Benoit. See Github #112 +- postgres7: fix system warning in MetaColumns() with schema. See http://phplens.com/lens/lensforum/msgs.php?id=19481 +- sqlite3: ServerInfo() now returns driver's version +- sqlite3: Fix wrong connection parameter in _connect(), thanks to diogotoscano. See Github #51 +- sqlite3: Fix FetchField, thanks to diogotoscano. See Github #53 +- sqlite3: Fix result-less SQL statements executed twice. See Github #99 +- sqlite3: use -1 for _numOfRows. See Github #151 +- xmlschema: Fix ExtractSchema() when given $prefix and $stripprefix parameters, thanks to peterdd. See Github #92 +- Convert languages files to UTF-8, thanks to Marc-Etienne Vargenau. See Github #32. + +## 5.19 - 23-Apr-2014 + +**NOTE:** +This release suffers from a [known issue with Associative Fetch Mode](https://github.com/ADOdb/ADOdb/issues/20) +(i.e. when $ADODB_FETCH_MODE is set to ADODB_FETCH_ASSOC). +It causes recordsets to return empty strings (no data) when using some database drivers. +The problem has been reported on MSSQL, Interbase and Foxpro, but possibly affects +other database types as well; all drivers derived from the above are also impacted. + +- adodb: GetRowAssoc will return null as required. See http://phplens.com/lens/lensforum/msgs.php?id=19289 +- adodb: Fix GetRowAssoc bug introduced in 5.17, causing function to return data from previous fetch for NULL fields. See http://phplens.com/lens/lensforum/msgs.php?id=17539 +- adodb: GetAssoc will return a zero-based array when 2nd column is null. See https://sourceforge.net/p/adodb/bugs/130/ +- adodb: Execute no longer ignores single parameters evaluating to false. See https://sourceforge.net/p/adodb/patches/32/ +- adodb: Fix LIMIT 1 clause in subquery gets stripped off. See http://phplens.com/lens/lensforum/msgs.php?id=17813 +- adodb-lib: Fix columns quoting bug. See https://sourceforge.net/p/adodb/bugs/127/ +- Added new ADODB_ASSOC_CASE_* constants. Thx to Damien Regad. +- sessions: changed lob handling to detect all variations of oci8 driver. +- ads: clear fields before fetching. See http://phplens.com/lens/lensforum/msgs.php?id=17539 +- mssqlnative: fixed many FetchField compat issues. See http://phplens.com/lens/lensforum/msgs.php?id=18464. Also date format changed to remove timezone. +- mssqlnative: Numerous fixes and improvements by Mark Newnham + - Driver supports SQL Server 2005, 2008 and 2012 + - Bigint data types mapped to I8 instead of I + - Reintroduced MetaColumns function + - On SQL Server 2012, makes use of new CREATE SEQUENCE statement + - FetchField caches metadata at initialization to improve performance + - etc. +- mssqlnative: Fix Insert ID on prepared statement, thanks to Mike Parks. See http://phplens.com/lens/lensforum/msgs.php?id=19079 +- mssql: timestamp format changed to `Y-m-d\TH:i:s` (ISO 8601) to make them independent from DATEFORMAT setting, as recommended on + [Microsoft TechNet](http://technet.microsoft.com/en-us/library/ms180878%28v=sql.105%29.aspx#StringLiteralDateandTimeFormats). +- mysql/mysqli: Fix ability for MetaTables to filter by table name, broken since 5.15. See http://phplens.com/lens/lensforum/msgs.php?id=19359 +- odbc: Fixed MetaTables and MetaPrimaryKeys definitions in odbc driver to match adoconnection class. +- odbc: clear fields before fetching. See http://phplens.com/lens/lensforum/msgs.php?id=17539 +- oci8: GetRowAssoc now works in ADODB_FETCH_ASSOC fetch mode +- oci8: MetaType and MetaForeignKeys argument count are now strict-standards compliant +- oci8: Added trailing `;` on trigger creation for sequence fields, prevents occurence of ORA-24344 +- oci8quercus: new oci8 driver with support for quercus jdbc data types. +- pdo: Fixed concat recursion bug in 5.3. See http://phplens.com/lens/lensforum/msgs.php?id=19285 +- pgsql: Default driver (postgres/pgsql) is now postgres8 +- pgsql: Fix output of BLOB (bytea) columns with PostgreSQL >= 9.0 +- pgsql: Fix handling of DEFAULT NULL columns in AlterColumnSQL +- pgsql: Fix mapping of error message to ADOdb error codes +- pgsql: Reset parameter number in Param() method when $name == false +- postgres8: New class/type with correct behavior for _insertid(). See Github #8 +- postgres9: Fixed assoc problem. See http://phplens.com/lens/lensforum/msgs.php?id=19296 +- sybase: Removed redundant sybase_connect() call in _connect(). See Github #3 +- sybase: Allow connection on custom port. See Github #9 +- sybase: Fix null values returned with ASSOC fetch mode. See Github #10 +- Added Composer support. See Github #7 + +## 5.18 - 3 Sep 2012 + +- datadict-postgres: Fixes bug in ALTER COL. See http://phplens.com/lens/lensforum/msgs.php?id=19202. +- datadict-postgres: fixed bugs in MetaType() checking $fieldobj properties. +- GetRowAssoc did not work with null values. Bug in 5.17. +- postgres9: New driver to better support PostgreSQL 9. Thx Glenn Herteg and Cacti team. +- sqlite3: Modified to support php 5.4. Thx Günter Weber [built.development#googlemail.com] +- adodb: When fetch mode is ADODB_FETCH_ASSOC, and we execute `$db->GetAssoc("select 'a','0'");` we get an error. Fixed. See http://phplens.com/lens/lensforum/msgs.php?id=19190 +- adodb: Caching directory permissions now configurable using global variable $ADODB_CACHE_PERMS. Default value is 0771. +- mysqli: SetCharSet() did not return true (success) or false (fail) correctly. Fixed. +- mysqli: changed dataProvider to 'mysql' so that MetaError and other shared functions will work. +- mssqlnative: Prepare() did not work previously. Now calling Prepare() will work but the sql is not actually compiled. Unfortunately bind params are passed to sqlsrv_prepare and not to sqlsrv_execute. make Prepare() and empty function, and we still execute the unprepared stmt. +- mysql: FetchField(-1), turns it is is not possible to retrieve the max_length. Set to -1. +- mysql-perf: Fixed "SHOW INNODB STATUS". Change to "SHOW ENGINE INNODB STATUS" + +## 5.17 - 18 May 2012 + +- Active Record: Removed trailing whitespace from adodb-active-record.inc.php. +- odbc: Added support for $database parameter in odbc Connect() function. E.g. $DB->Connect($dsn_without_db, $user, $pwd, $database). + Previously $database had to be left blank and the $dsn was used to pass in this parameter. +- oci8: Added better empty($rs) error handling to metaindexes(). +- oci8: Changed to use newer oci API to support PHP 5.4. +- adodb.inc.php: Changed GetRowAssoc to more generic code that will work in all scenarios. + +## 5.16 - 26 March 2012 + +- mysqli: extra mysqli_next_result() in close() removed. See http://phplens.com/lens/lensforum/msgs.php?id=19100 +- datadict-oci8: minor typo in create sequence trigger fixed. See http://phplens.com/lens/lensforum/msgs.php?id=18879. +- security: safe date parsing changes. Does not impact security, these are code optimisations. Thx Saithis. +- postgres, oci8, oci8po, db2oci: Param() function parameters inconsistent with base class. $type='C' missing. Fixed. +- active-record: locked bug fixed. http://phplens.com/lens/lensforum/msgs.php?phplens_forummsg=new&id=19073 +- mysql, mysqli and informix: added MetaProcedures. Metaprocedures allows to retrieve an array list of all procedures in database. http://phplens.com/lens/lensforum/msgs.php?id=18414 +- Postgres7: added support for serial data type in MetaColumns(). + +## 5.15 - 19 Jan 2012 + +- pdo: fix ErrorMsg() to detect errors correctly. Thx Jens. +- mssqlnative: added another check for $this->fields array exists. +- mssqlnative: bugs in FetchField() fixed. See http://phplens.com/lens/lensforum/msgs.php?id=19024 +- DBDate and DBTimeStamp had sql injection bug. Fixed. Thx Saithis +- mysql and mysqli: MetaTables() now identifies views and tables correctly. +- Added function adodb_time() to adodb-time.inc.php. Generates current time in unsigned integer format. + +## 5.14 - 8 Sep 2011 + +- mysqli: fix php compilation bug. +- postgres: bind variables did not work properly. Fixed. +- postgres: blob handling bug in _decode. Fixed. +- ActiveRecord: if a null field was never updated, activerecord would still update the record. Fixed. +- ActiveRecord: 1 char length string never quoted. Fixed. +- LDAP: Connection string ldap:// and ldaps:// did not work. Fixed. + +## 5.13 - 15 Aug 2011 + +- Postgres: Fix in 5.12 was wrong. Uses pg_unescape_bytea() correctly now in _decode. +- GetInsertSQL/GetUpdateSQL: Now $ADODB_QUOTE_FIELDNAMES allows you to define 'NATIVE', 'UPPER', 'LOWER'. If set to true, will default to 'UPPER'. +- mysqli: added support for persistent connections 'p:'. +- mssqlnative: ADODB_FETCH_BOTH did not work properly. Fixed. +- mssqlnative: return values for stored procedures where not returned! Fixed. See http://phplens.com/lens/lensforum/msgs.php?id=18919 +- mssqlnative: timestamp and fetchfield bugs fixed. http ://phplens.com/lens/lensforum/msgs.php?id=18453 + +## 5.12 - 30 June 2011 + +- Postgres: Added information_schema support for postgresql. +- Postgres: Use pg_unescape_bytea() in _decode. +- Fix bulk binding with oci8. http://phplens.com/lens/lensforum/msgs.php?id=18786 +- oci8 perf: added wait evt monitoring. Also db cache advice now handles multiple buffer pools properly. +- sessions2: Fixed setFetchMode problem. +- sqlite: Some DSN connection settings were not parsed correctly. +- mysqli: now GetOne obeys $ADODB_GETONE_EOF; +- memcache: compress option did not work. Fixed. See http://phplens.com/lens/lensforum/msgs.php?id=18899 + +## 5.11 - 5 May 2010 + +- mysql: Fixed GetOne() to return null if no records returned. +- oci8 perf: added stats on sga, rman, memory usage, and flash in performance tab. +- odbtp: Now you can define password in $password field of Connect()/PConnect(), and it will add it to DSN. +- Datadict: altering columns did not consider the scale of the column. Now it does. +- mssql: Fixed problem with ADODB_CASE_ASSOC causing multiple versions of column name appearing in recordset fields. +- oci8: Added missing & to refLob. +- oci8: Added obj->scale to FetchField(). +- oci8: Now you can get column info of a table in a different schema, e.g. MetaColumns("schema.table") is supported. +- odbc_mssql: Fixed missing $metaDatabasesSQL. +- xmlschema: Changed declaration of create() to create($xmls) to fix compat problems. Also changed constructor adoSchema() to pass in variable instead of variable reference. +- ado5: Fixed ado5 exceptions to only display errors when $this->debug=true; +- Added DSN support to sessions2.inc.php. +- adodb-lib.inc.php. Fixed issue with _adodb_getcount() not using $secs2cache parameter. +- adodb active record. Fixed caching bug. See http://phplens.com/lens/lensforum/msgs.php?id=18288. +- db2: fixed ServerInfo(). +- adodb_date: Added support for format 'e' for TZ as in adodb_date('e') +- Active Record: If you have a field which is a string field (with numbers in) and you add preceding 0's to it the adodb library does not pick up the fact that the field has changed because of the way php's == works (dodgily). The end result is that it never gets updated into the database - fix by Matthew Forrester (MediaEquals). [matthew.forrester#mediaequals.com] +- Fixes RowLock() and MetaIndexes() inconsistencies. See http://phplens.com/lens/lensforum/msgs.php?id=18236 +- Active record support for postgrseql boolean. See http://phplens.com/lens/lensforum/msgs.php?id=18246 +- By default, Execute 2D array is disabled for security reasons. Set $conn->bulkBind = true to enable. See http://phplens.com/lens/lensforum/msgs.php?id=18270. Note this breaks backward compat. +- MSSQL: fixes for 5.2 compat. http://phplens.com/lens/lensforum/msgs.php?id=18325 +- Changed Version() to return a string instead of a float so it correctly returns 5.10 instead of 5.1. + +## 5.10 - 10 Nov 2009 + +- Fixed memcache to properly support $rs->timeCreated. +- adodb-ado.inc.php: Added BigInt support for PHP5. Will return float instead to support large numbers. Thx nasb#mail.goo.ne.jp. +- adodb-mysqli.inc.php: mysqli_multi_query is now turned off by default. To turn it on, use $conn->multiQuery = true; This is because of the risks of sql injection. See http://phplens.com/lens/lensforum/msgs.php?id=18144 +- New db2oci driver for db2 9.7 when using PL/SQL mode. Allows oracle style :0, :1, :2 bind parameters which are remapped to ? ? ?. +- adodb-db2.inc.php: fixed bugs in MetaTables. SYS owner field not checked properly. Also in $conn->Connect($dsn, null, null, $schema) and PConnect($dsn, null, null, $schema), we do a SET SCHEMA=$schema if successful connection. +- adodb-mysqli.inc.php: Now $rs->Close() closes all pending next resultsets. Thx Clifton mesmackgod#gmail.com +- Moved _CreateCache() from PConnect()/Connect() to CacheExecute(). Suggested by Dumka. +- Many bug fixes to adodb-pdo_sqlite.inc.php and new datadict-sqlite.inc.php. Thx Andrei B. [andreutz#mymail.ro] +- Removed usage of split (deprecated in php 5.3). Thx david#horizon-nigh.org. +- Fixed RowLock() parameters to comply with PHP5 strict mode in multiple drivers. + +## 5.09 - 25 June 2009 + +- Active Record: You can force column names to be quoted in INSERT and UPDATE statements, typically because you are using reserved words as column names by setting ADODB_Active_Record::$_quoteNames = true; +- Added memcache and cachesecs to DSN. e.g. + + ``` php + # we have a memcache servers mem1,mem2 on port 8888, compression=off and cachesecs=120 + $dsn = 'mysql://user:pwd@localhost/mydb?memcache=mem1,mem2:8888:0&cachesecs=120'; + ``` + +- Fixed up MetaColumns and MetaPrimaryIndexes() for php 5.3 compat. Thx http://adodb.pastebin.com/m52082b16 +- The postgresql driver's OffsetDate() apparently does not work with postgres 8.3. Fixed. +- Added support for magic_quotes_sybase in qstr() and addq(). Thanks Eloy and Sam Moffat. +- The oci8 driver did not handle LOBs properly when binding. Fixed. See http://phplens.com/lens/lensforum/msgs.php?id=17991. +- Datadict: In order to support TIMESTAMP with subsecond accuracy, added to datadict the new TS type. Supported by mssql, postgresql and oci8 (oracle). Also changed oci8 $conn->sysTimeStamp to use 'SYSTIMESTAMP' instead of 'SYSDATE'. Should be backwards compat. +- Added support for PHP 5.1+ DateTime objects in DBDate and DBTimeStamp. This means that dates and timestamps will be managed by DateTime objects if you are running PHP 5.1+. +- Added new property to postgres64 driver to support returning I if type is unique int called $db->uniqueIisR, defaulting to true. See http://phplens.com/lens/lensforum/msgs.php?id=17963 +- Added support for bindarray in adodb_GetActiveRecordsClass with SelectLimit in adodb-active-record.inc.php. +- Transactions now allowed in ado_access driver. Thx to petar.petrov.georgiev#gmail.com. +- Sessions2 garbage collection is now much more robust. We perform ORDER BY to prevent deadlock in adodb-sessions2.inc.php. +- Fixed typo in pdo_sqlite driver. + +## 5.08a - 17 Apr 2009 + +- Fixes wrong version number string. +- Incorrect + in adodb-datadict.inc.php removed. +- Fixes missing OffsetDate() function in pdo. Thx paul#mantisforge.org. + +## 5.08 - 17 Apr 2009 + +- adodb-sybase.inc.php driver. Added $conn->charSet support. Thx Luis Henrique Mulinari (luis.mulinari#gmail.com) +- adodb-ado5.inc.php. Fixed some bind param issues. Thx Jirka Novak. +- adodb-ado5.inc.php. Now has improved error handling. +- Fixed typo in adodb-xmlschema03.inc.php. See XMLS_EXISTING_DATA, line 1501. Thx james johnson. +- Made $inputarr optional for _query() in all drivers. +- Fixed spelling mistake in flushall() in adodb.inc.ophp. +- Fixed handling of quotes in adodb_active_record::doquote. Thx Jonathan Hohle (jhohle#godaddy.com). +- Added new index parameter to adodb_active_record::setdatabaseadaptor. Thx Jonathan Hohle +- Fixed & readcache() reference compat problem with php 5.3 in adodb.Thx Jonathan Hohle. +- Some minor $ADODB_CACHE_CLASS definition issues in adodb.inc.php. +- Added Reset() function to adodb_active_record. Thx marcus. +- Minor dsn fix for pdo_sqlite in adodb.inc.php. Thx Sergey Chvalyuk. +- Fixed adodb-datadict _CreateSuffix() inconsistencies. Thx Chris Miller. +- Option to delete old fields $dropOldFlds in datadict ChangeTableSQL($table, $flds, $tableOptions, $dropOldFlds=false) added. Thx Philipp Niethammer. +- Memcache caching did not expire properly. Fixed. +- MetaForeignKeys for postgres7 driver changed from adodb_movenext to $rs->MoveNext (also in 4.99) +- Added support for ldap and ldaps url format in ldap driver. E.g. ldap://host:port/dn?attributes?scope?filter?extensions + +## 5.07 - 26 Dec 2008 + +- BeginTrans/CommitTrans/RollbackTrans return true/false correctly on success/failure now for mssql, odbc, oci8, mysqlt, mysqli, postgres, pdo. +- Replace() now quotes all non-null values including numeric ones. +- Postgresql qstr() now returns booleans as *true* and *false* without quotes. +- MetaForeignKeys in mysql and mysqli drivers had this problem: A table can have two foreign keys pointing to the same column in the same table. The original code will incorrectly report only the last column. Fixed. https://sourceforge.net/tracker/index.php?func=detail&aid=2287278&group_id=42718&atid=433976 +- Passing in full ado connection string in $argHostname with ado drivers was failing in adodb5 due to bug. Fixed. +- Fixed memcachelib flushcache and flushall bugs. Also fixed possible timeCreated = 0 problem in readcache. (Also in adodb 4.992). Thanks AlexB_UK (alexbarnes#hotmail.com). +- Fixed a notice in adodb-sessions2.inc.php, in _conn(). Thx bober m.derlukiewicz#rocktech.remove_me.pl; +- ADOdb Active Record: Fixed some issues with incompatible fetch modes (ADODB_FETCH_ASSOC) causing problems in UpdateActiveTable(). +- ADOdb Active Record: Added support for functions that support predefining one-to-many relationships: + _ClassHasMany ClassBelongsTo TableHasMany TableBelongsTo TableKeyHasMany TableKeyBelongsTo_. +- You can also define your child/parent class in these functions, instead of the default ADODB_Active_Record. Thx Arialdo Martini & Chris R for idea. +- ADOdb Active Record: HasMany hardcoded primary key to "id". Fixed. +- Many pdo and pdo-sqlite fixes from Sid Dunayer [sdunayer#interserv.com]. +- CacheSelectLimit not working for mssql. Fixed. Thx AlexB. +- The rs2html function did not display hours in timestamps correctly. Now 24hr clock used. +- Changed ereg* functions to use preg* functions as ereg* is deprecated in PHP 5.3. Modified sybase and postgresql drivers. + +## 5.06 - 16 Oct 2008 + +- Added driver adodb-pdo_sqlite.inc.php. Thanks Diogo Toscano (diogo#scriptcase.net) for the code. +- Added support for [one-to-many relationships](docs-active-record.htm#onetomany) with BelongsTo() and HasMany() in adodb_active_record. +- Added BINARY type to mysql.inc.php (also in 4.991). +- Added support for SelectLimit($sql,-1,100) in oci8. (also in 4.991). +- New $conn->GetMedian($table, $field, $where='') to get median account no. (also in 4.991) +- The rs2html() function in tohtml.inc.php did not handle dates with ':' in it properly. Fixed. (also in 4.991) +- Added support for connecting to oci8 using `$DB->Connect($ip, $user, $pwd, "SID=$sid");` (also in 4.991) +- Added mysql type 'VAR_STRING' to MetaType(). (also in 4.991) +- The session and session2 code supports setfetchmode assoc properly now (also in 4.991). +- Added concat support to pdo. Thx Andrea Baron. +- Changed db2 driver to use format `Y-m-d H-i-s` for datetime instead of `Y-m-d-H-i-s` which was legacy from odbc_db2 conversion. +- Removed vestigal break on adodb_tz_offset in adodb-time.inc.php. +- MetaForeignKeys did not work for views in MySQL 5. Fixed. +- Changed error handling in GetActiveRecordsClass. +- Added better support for using existing driver when $ADODB_NEWCONNECTION function returns false. +- In _CreateSuffix in adodb-datadict.inc.php, adding unsigned variable for mysql. +- In adodb-xmlschema03.inc.php, changed addTableOpt to include db name. +- If bytea blob in postgresql is null, empty string was formerly returned. Now null is returned. +- Changed db2 driver CreateSequence to support $start parameter. +- rs2html() now does not add nbsp to end if length of string > 0 +- The oci8po FetchField() now only lowercases field names if ADODB_ASSOC_CASE is set to 0. +- New mssqlnative drivers for php. TQ Garrett Serack of M'soft. [Download](http://www.microsoft.com/downloads/details.aspx?FamilyId=61BF87E0-D031-466B-B09A-6597C21A2E2A&displaylang=en) mssqlnative extension. Note that this is still in beta. +- Fixed bugs in memcache support. +- You can now change the return value of GetOne if no records are found using the global variable $ADODB_GETONE_EOF. The default is null. To change it back to the pre-4.99/5.00 behaviour of false, set $ADODB_GETONE_EOF = false; +- In Postgresql 8.2/8.3 MetaForeignkeys did not work. Fixed William Kolodny William.Kolodny#gt-t.net + +## 5.05 - 11 Jul 2008 + +Released together with [v4.990](changelog_v4.x.md#4990---11-jul-2008) + +- Added support for multiple recordsets in mysqli , thanks to Geisel Sierote geisel#4up.com.br. See http://phplens.com/lens/lensforum/msgs.php?id=15917 +- Malcolm Cook added new Reload() function to Active Record. See http://phplens.com/lens/lensforum/msgs.php?id=17474 +- Thanks Zoltan Monori (monzol#fotoprizma.hu) for bug fixes in iterator, SelectLimit, GetRandRow, etc. +- Under heavy loads, the performance monitor for oci8 disables Ixora views. +- Fixed sybase driver SQLDate to use str_replace(). Also for adodb5, changed sybase driver UnixDate and UnixTimeStamp calls to static. +- Changed oci8 lob handler to use & reference `$this->_refLOBs[$numlob]['VAR'] = &$var`. +- We now strtolower the get_class() function in PEAR::isError() for php5 compat. +- CacheExecute did not retrieve cache recordsets properly for 5.04 (worked in 4.98). Fixed. +- New ADODB_Cache_File class for file caching defined in adodb.inc.php. +- Farsi language file contribution by Peyman Hooshmandi Raad (phooshmand#gmail.com) +- New API for creating your custom caching class which is stored in $ADODB_CACHE: + + ``` php + include "/path/to/adodb.inc.php"; + $ADODB_CACHE_CLASS = 'MyCacheClass'; + class MyCacheClass extends ADODB_Cache_File + { + function writecache($filename, $contents,$debug=false) {...} + function &readcache($filename, &$err, $secs2cache, $rsClass) { ...} + : + } + $DB = NewADOConnection($driver); + $DB->Connect(...); ## MyCacheClass created here and stored in $ADODB_CACHE global variable. + $data = $rs->CacheGetOne($sql); ## MyCacheClass is used here for caching... + ``` + +- Memcache supports multiple pooled hosts now. Only if none of the pooled servers + can be contacted will a connect error be generated. Usage example below: + + ``` php + $db = NewADOConnection($driver); + $db->memCache = true; /// should we use memCache instead of caching in files + $db->memCacheHost = array($ip1, $ip2, $ip3); /// $db->memCacheHost = $ip1; still works + $db->memCachePort = 11211; /// this is default memCache port + $db->memCacheCompress = false; /// Use 'true' to store the item compressed (uses zlib) + $db->Connect(...); + $db->CacheExecute($sql); + ``` + +## 5.04 - 13 Feb 2008 + +Released together with [v4.98](changelog_v4.x.md#498---13-feb-2008) + +- Fixed adodb_mktime problem which causes a performance bottleneck in $hrs. +- Added mysqli support to adodb_getcount(). +- Removed MYSQLI_TYPE_CHAR from MetaType(). + +## 5.03 - 22 Jan 2008 + +Released together with [v4.97](changelog_v4.x.md#497---22-jan-2008) + +- Active Record: $ADODB_ASSOC_CASE=1 did not work properly. Fixed. +- Modified Fields() in recordset class to support display null fields in FetchNextObject(). +- In ADOdb5, active record implementation, we now support column names with spaces in them - we autoconvert the spaces to _ using __set(). Thx Daniel Cook. http://phplens.com/lens/lensforum/msgs.php?id=17200 +- Removed $arg3 from mysqli SelectLimit. See http://phplens.com/lens/lensforum/msgs.php?id=16243. Thx Zsolt Szeberenyi. +- Changed oci8 FetchField, which returns the max_length of BLOB/CLOB/NCLOB as 4000 (incorrectly) to -1. +- CacheExecute would sometimes return an error on Windows if it was unable to lock the cache file. This is harmless and has been changed to a warning that can be ignored. Also adodb_write_file() code revised. +- ADOdb perf code changed to only log sql if execution time >= 0.05 seconds. New $ADODB_PERF_MIN variable holds min sql timing. Any SQL with timing value below this and is not causing an error is not logged. +- Also adodb_backtrace() now traces 1 level deeper as sometimes actual culprit function is not displayed. +- Fixed a group by problem with adodb_getcount() for db's which are not postgres/oci8 based. +- Changed mssql driver Parameter() from SQLCHAR to SQLVARCHAR: case 'string': $type = SQLVARCHAR; break. +- Problem with mssql driver in php5 (for adodb 5.03) because some functions are not static. Fixed. + +## 5.02 - 24 Sept 2007 + +Released together with [v4.96](changelog_v4.x.md#496---24-sept-2007) + +- ADOdb perf for oci8 now has non-table-locking code when clearing the sql. Slower but better transparency. Added in 4.96a and 5.02a. +- Fix adodb count optimisation. Preg_match did not work properly. Also rewrote the ORDER BY stripping code in _adodb_getcount(), adodb-lib.inc.php. +- SelectLimit for oci8 not optimal for large recordsets when offset=0. Changed $nrows check. +- Active record optimizations. Added support for assoc arrays in Set(). +- Now GetOne returns null if EOF (no records found), and false if error occurs. Use ErrorMsg()/ErrorNo() to get the error. +- Also CacheGetRow and CacheGetCol will return false if error occurs, or empty array() if EOF, just like GetRow and GetCol. +- Datadict now allows changing of types which are not resizable, eg. VARCHAR to TEXT in ChangeTableSQL. -- Mateo Tibaquirá +- Added BIT data type support to adodb-ado.inc.php and adodb-ado5.inc.php. +- Ldap driver did not return actual ldap error messages. Fixed. +- Implemented GetRandRow($sql, $inputarr). Optimized for Oci8. +- Changed adodb5 active record to use static SetDatabaseAdapter() and removed php4 constructor. Bas van Beek bas.vanbeek#gmail.com. +- Also in adodb5, changed adodb-session2 to use static function declarations in class. Thx Daniel Berlin. +- Added "Clear SQL Log" to bottom of Performance screen. +- Sessions2 code echo'ed directly to the screen in debug mode. Now uses ADOConnection::outp(). +- In mysql/mysqli, qstr(null) will return the string `null` instead of empty quoted string `''`. +- postgresql optimizeTable in perf-postgres.inc.php added by Daniel Berlin (mail#daniel-berlin.de) +- Added 5.2.1 compat code for oci8. +- Changed @@identity to SCOPE_IDENTITY() for multiple mssql drivers. Thx Stefano Nari. +- Code sanitization introduced in 4.95 caused problems in European locales (as float 3.2 was typecast to 3,2). Now we only sanitize if is_numeric fails. +- Added support for customizing ADORecordset_empty using $this->rsPrefix.'empty'. By Josh Truwin. +- Added proper support for ALterColumnSQL for Postgresql in datadict code. Thx. Josh Truwin. +- Added better support for MetaType() in mysqli when using an array recordset. +- Changed parser for pgsql error messages in adodb-error.inc.php to case-insensitive regex. + +## 5.01 - 17 May 2007 + +Released together with [v4.95](changelog_v4.x.md#495---17-may-2007) + +- CacheFlush debug outp() passed in invalid parameters. Fixed. +- Added Thai language file for adodb. Thx Trirat Petchsingh rosskouk#gmail.com and Marcos Pont +- Added zerofill checking support to MetaColumns for mysql and mysqli. +- CacheFlush no longer deletes all files/directories. Only *.cache files deleted. +- DB2 timestamp format changed to `var $fmtTimeStamp = "'Y-m-d-H:i:s'";` +- Added some code sanitization to AutoExecute in adodb-lib.inc.php. +- Due to typo, all connections in adodb-oracle.inc.php would become persistent, even non-persistent ones. Fixed. +- Oci8 DBTimeStamp uses 24 hour time for input now, so you can perform string comparisons between 2 DBTimeStamp values. +- Some PHP4.4 compat issues fixed in adodb-session2.inc.php +- For ADOdb 5.01, fixed some adodb-datadict.inc.php MetaType compat issues with PHP5. +- The $argHostname was wiped out in adodb-ado5.inc.php. Fixed. +- Adodb5 version, added iterator support for adodb_recordset_empty. +- Adodb5 version,more error checking code now will use exceptions if available. diff --git a/app/vendor/adodb/adodb-php/docs/changelog_v2.x.md b/app/vendor/adodb/adodb-php/docs/changelog_v2.x.md new file mode 100644 index 000000000..000dcd5ad --- /dev/null +++ b/app/vendor/adodb/adodb-php/docs/changelog_v2.x.md @@ -0,0 +1,531 @@ +# ADOdb old Changelog - v2.x and older + +See the [Current Changelog](changelog.md). + + +## 2.91 - 3 Jan 2003 + +- Revised PHP version checking to use $ADODB_PHPVER with legal values 0x4000, 0x4050, 0x4200, 0x4300. +- Added support for bytea fields and oid blobs in postgres by allowing BlobDecode() to detect and convert non-oid fields. Also added BlobEncode to postgres when you want to encode oid blobs. +- Added blobEncodeType property for connections to inform phpLens what encoding method to use for blobs. +- Added BlobDecode() and BlobEncode() to base ADOConnection class. +- Added umask() to _gencachename() when creating directories. +- Added charPage for ado drivers, so you can set the code page. + ``` +$conn->charPage = CP_UTF8; +$conn->Connect($dsn); + ``` +- Modified _seek in mysql to check for num rows=0. +- Added to metatypes new informix types for IDS 9.30\. Thx Fernando Ortiz. +- _maxrecordcount returned in CachePageExecute $rsreturn +- Fixed sybase cacheselectlimit( ) problems +- MetaColumns() max_length should use precision for types X and C for ms access. Fixed. +- Speedup of odbc non-SELECT sql statements. +- Added support in MetaColumns for Wide Char types for ODBC. We halve max_length if unicode/wide char. +- Added 'B' to types handled by GetUpdateSQL/GetInsertSQL. +- Fixed warning message in oci8 driver with $persist variable when using PConnect. + +## 2.90 - 11 Dec 2002 + +- Mssql and mssqlpo and oci8po now support ADODB_ASSOC_CASE. +- Now MetaType() can accept a field object as the first parameter. +- New $arr = $db->ServerInfo( ) function. Returns $arr['description'] which is the string description, and $arr['version']. +- PostgreSQL and MSSQL speedups for insert/updates. +- Implemented new SetFetchMode() that removes the need to use $ADODB_FETCH_MODE. Each connection has independant fetchMode. +- ADODB_ASSOC_CASE now defaults to 2, use native defaults. This is because we would break backward compat for too many applications otherwise. +- Patched encrypted sessions to use replace() +- The qstr function supports quoting of nulls when escape character is \ +- Rewrote bits and pieces of session code to check for time synch and improve reliability. +- Added property ADOConnection::hasTransactions = true/false; +- Added CreateSequence and DropSequence functions +- Found misplaced MoveNext() in adodb-postgres.inc.php. Fixed. +- Sybase SelectLimit not reliable because 'set rowcount' not cached - fixed. +- Moved ADOConnection to adodb-connection.inc.php and ADORecordSet to adodb-recordset.inc.php. This allows us to use doxygen to generate documentation. Doxygen doesn't like the classes in the main adodb.inc.php file for some mysterious reason. + +## 2.50 - 14 Nov 2002 + +- Added transOff and transCnt properties for disabling (transOff = true) and tracking transaction status (transCnt>0). +- Added inputarray handling into _adodb_pageexecute_all_rows - "Ross Smith" RossSmith#bnw.com. +- Fixed postgresql inconsistencies in date handling. +- Added support for mssql_fetch_assoc. +- Fixed $ADODB_FETCH_MODE bug in odbc MetaTables() and MetaPrimaryKeys(). +- Accidentally declared UnixDate() twice, making adodb incompatible with php 4.3.0\. Fixed. +- Fixed pager problems with some databases that returned -1 for _currentRow on MoveLast() by switching to MoveNext() in adodb-lib.inc.php. +- Also fixed uninited $discard in adodb-lib.inc.php. + +## 2.43 - 25 Oct 2002 + +- Added ADODB_ASSOC_CASE constant to better support ibase and odbc field names. +- Added support for NConnect() for oracle OCINLogin. +- Fixed NumCols() bug. +- Changed session handler to use Replace() on write. +- Fixed oci8 SelectLimit aggregate function bug again. +- Rewrote pivoting code. + +## 2.42 - 4 Oct 2002 + +- Fixed ibase_fetch() problem with nulls. Also interbase now does automatic blob decoding, and is backward compatible. Suggested by Heinz Hombergs heinz#hhombergs.de. +- Fixed postgresql MoveNext() problems when called repeatedly after EOF. Also suggested by Heinz Hombergs. +- PageExecute() does not rewrite queries if SELECT DISTINCT is used. Requested by hans#velum.net +- Added additional fixes to oci8 SelectLimit handling with aggregate functions - thx to Christian Bugge for reporting the problem. + +## 2.41 - 2 Oct 2002 + +- Fixed ADODB_COUNTRECS bug in odbc. Thx to Joshua Zoshi jzoshi#hotmail.com. +- Increased buffers for adodb-csvlib.inc.php for extremely long sql from 8192 to 32000. +- Revised pivottable.inc.php code. Added better support for aggregate fields. +- Fixed mysql text/blob types problem in MetaTypes base class - thx to horacio degiorgi. +- Added SQLDate($fmt,$date) function, which allows an sql date format string to be generated - useful for group by's. +- Fixed bug in oci8 SelectLimit when offset>100. + +## 2.40 - 4 Sept 2002 + +- Added new NLS_DATE_FORMAT property to oci8\. Suggested by Laurent NAVARRO ln#altidev.com +- Now use bind parameters in oci8 selectlimit for better performance. +- Fixed interbase replaceQuote for dialect != 1\. Thx to "BEGUIN Pierre-Henri - INFOCOB" phb#infocob.com. +- Added white-space check to QA. +- Changed unixtimestamp to support fractional seconds (we always round down/floor the seconds). Thanks to beezly#beezly.org.uk. +- Now you can set the trigger_error type your own user-defined type in adodb-errorhandler.inc.php. Suggested by Claudio Bustos clbustos#entelchile.net. +- Added recordset filters with rsfilter.inc.php. + $conn->_rs2rs does not create a new recordset when it detects it is of type array. Some trickery there as there seems to be a bug in Zend Engine +- Added render_pagelinks to adodb-pager.inc.php. Code by "Pablo Costa" pablo#cbsp.com.br. +- MetaType() speedup in adodb.inc.php by using hashing instead of switch. Best performance if constant arrays are supported, as they are in PHP5. +- adodb-session.php now updates only the expiry date if the crc32 check indicates that the data has not been modified. + +## 2.31 - 20 Aug 2002 + +- Made changes to pivottable.inc.php due to daniel lucuzaeu's suggestions (we sum the pivottable column if desired). +- Fixed ErrorNo() in postgres so it does not depend on _errorMsg property. +- Robert Tuttle added support for oracle cursors. See ExecuteCursor(). +- Fixed Replace() so it works with mysql when updating record where data has not changed. Reported by Cal Evans (cal#calevans.com). + +## 2.30 - 1 Aug 2002 + +- Added pivottable.inc.php. Thanks to daniel.lucazeau#ajornet.com for the original concept. +- Added ADOConnection::outp($msg,$newline) to output error and debugging messages. Now you can override this using the ADODB_OUTP constant and use your own output handler. +- Changed == to === for 'null' comparison. Reported by ericquil#yahoo.com +- Fixed mssql SelectLimit( ) bug when distinct used. + +## 2.30 - 1 Aug 2002 + +- New GetCol() and CacheGetCol() from ross#bnw.com that returns the first field as a 1 dim array. +- We have an empty recordset, but RecordCount() could return -1\. Fixed. Reported by "Jonathan Polansky" jonathan#polansky.com. +- We now check for session variable changes using strlen($sessval).crc32($sessval). Formerly we only used crc32(). +- Informix SelectLimit() problem with $ADODB_COUNTRECS fixed. +- Fixed informix SELECT FIRST x DISTINCT, and not SELECT DISTINCT FIRST x - reported by F Riosa +- Now default adodb error handlers ignores error if @ used. +- If you set $conn->autoRollback=true, we auto-rollback persistent connections for odbc, mysql, oci8, mssql. Default for autoRollback is false. No need to do so for postgres. As interbase requires a transaction id (what a flawed api), we don't do it for interbase. +- Changed PageExecute() to use non-greedy preg_match when searching for "FROM" keyword. + +## 2.20 - 9 July 2002 + +- Added CacheGetOne($secs2cache,$sql), CacheGetRow($secs2cache,$sql), CacheGetAll($secs2cache,$sql). +- Added $conn->OffsetDate($dayFraction,$date=false) to generate sql that calcs date offsets. Useful for scheduling appointments. +- Added connection properties: leftOuter, rightOuter that hold left and right outer join operators. +- Added connection property: ansiOuter to indicate whether ansi outer joins supported. +- New driver _mssqlpo_, the portable mssql driver, which converts string concat operator from || to +. +- Fixed ms access bug - SelectLimit() did not support ties - fixed. +- Karsten Kraus (Karsten.Kraus#web.de), contributed error-handling code to ADONewConnection. Unfortunately due to backward compat problems, had to rollback most of the changes. +- Added new parameter to GetAssoc() to allow returning an array of key-value pairs, ignoring any additional columns in the recordset. Off by default. +- Corrected mssql $conn->sysDate to return only date using convert(). +- CacheExecute() improved debugging output. +- Changed rs2html() so newlines are converted to BR tags. Also optimized rs2html() based on feedback by "Jerry Workman" jerry#mtncad.com. +- Added support for Replace() with Interbase, using DELETE and INSERT. +- Some minor optimizations (mostly removing & references when passing arrays). +- Changed GenID() to allows id's larger than the size of an integer. +- Added force_session property to oci8 for better updateblob() support. +- Fixed PageExecute() which did not work properly with sql containing GROUP BY. + +## 2.12 - 12 June 2002 + +- Added toexport.inc.php to export recordsets in CSV and tab-delimited format. +- CachePageExecute() does not work - fixed - thx John Huong. +- Interbase aliases not set properly in FetchField() - fixed. Thx Stefan Goethals. +- Added cache property to adodb pager class. The number of secs to cache recordsets. +- SQL rewriting bug in pageexecute() due to skipping of newlines due to missing /s modifier. Fixed. +- Max size of cached recordset due to a bug was 256000 bytes. Fixed. +- Speedup of 1st invocation of CacheExecute() by tuning code. +- We compare $rewritesql with $sql in pageexecute code in case of rewrite failure. + +## 2.11 - 7 June 2002 + +- Fixed PageExecute() rewrite sql problem - COUNT(*) and ORDER BY don't go together with mssql, access and postgres. Thx to Alexander Zhukov alex#unipack.ru +- DB2 support for CHARACTER type added - thx John Huong huongch#bigfoot.com +- For ado, $argProvider not properly checked. Fixed - kalimero#ngi.it +- Added $conn->Replace() function for update with automatic insert if the record does not exist. Supported by all databases except interbase. + +## 2.10 - 4 June 2002 + +- Added uniqueSort property to indicate mssql ORDER BY cols must be unique. +- Optimized session handler by crc32 the data. We only write if session data has changed. +- adodb_sess_read in adodb-session.php now returns ''correctly - thanks to Jorma Tuomainen, webmaster#wizactive.com +- Mssql driver did not throw EXECUTE errors correctly because ErrorMsg() and ErrorNo() called in wrong order. Pointed out by Alexios Fakos. Fixed. +- Changed ado to use client cursors. This fixes BeginTran() problems with ado. +- Added handling of timestamp type in ado. +- Added to ado_mssql support for insert_id() and affected_rows(). +- Added support for mssql.datetimeconvert=0, available since php 4.2.0. +- Made UnixDate() less strict, so that the time is ignored if present. +- Changed quote() so that it checks for magic_quotes_gpc. +- Changed maxblobsize for odbc to default to 64000. + +## 2.00 - 13 May 2002 + +- Added drivers _informix72_ for pre-7.3 versions, and _oci805_ for oracle 8.0.5, and postgres64 for postgresql 6.4 and earlier. The postgres and postgres7 drivers are now identical. +- Interbase now partially supports ADODB_FETCH_BOTH, by defaulting to ASSOC mode. +- Proper support for blobs in mssql. Also revised blob support code is base class. Now UpdateBlobFile() calls UpdateBlob() for consistency. +- Added support for changed odbc_fetch_into api in php 4.2.0 with $conn->_has_stupid_odbc_fetch_api_change. +- Fixed spelling of tablock locking hint in GenID( ) for mssql. +- Added RowLock( ) to several databases, including oci8, informix, sybase, etc. Fixed where error in mssql RowLock(). +- Added sysDate and sysTimeStamp properties to most database drivers. These are the sql functions/constants for that database that return the current date and current timestamp, and are useful for portable inserts and updates. +- Support for RecordCount() caused date handling in sybase and mssql to break. Fixed, thanks to Toni Tunkkari, by creating derived classes for ADORecordSet_array for both databases. Generalized using arrayClass property. Also to support RecordCount(), changed metatype handling for ado drivers. Now the type returned in FetchField is no longer a number, but the 1-char data type returned by MetaType. At the same time, fixed a lot of date handling. Now mssql support dmy and mdy date formats. Also speedups in sybase and mssql with preg_match and ^ in date/timestamp handling. Added support in sybase and mssql for 24 hour clock in timestamps (no AM/PM). +- Extensive revisions to informix driver - thanks to Samuel CARRIERE samuel_carriere#hotmail.com +- Added $ok parameter to CommitTrans($ok) for easy rollbacks. +- Fixed odbc MetaColumns and MetaTables to save and restore $ADODB_FETCH_MODE. +- Some odbc drivers did not call the base connection class constructor. Fixed. +- Fixed regex for GetUpdateSQL() and GetInsertSQL() to support more legal character combinations. + +## 1.99 - 21 April 2002 + +- Added emulated RecordCount() to all database drivers if $ADODB_COUNTRECS = true (which it is by default). Inspired by Cristiano Duarte (cunha17#uol.com.br). +- Unified stored procedure support for mssql and oci8\. Parameter() and PrepareSP() functions implemented. +- Added support for SELECT FIRST in informix, modified hasTop property to support this. +- Changed csv driver to handle updates/deletes/inserts properly (when Execute() returns true). Bind params also work now, and raiseErrorFn with csv driver. Added csv driver to QA process. +- Better error checking in oci8 UpdateBlob() and UpdateBlobFile(). +- Added TIME type to MySQL - patch by Manfred h9125297#zechine.wu-wien.ac.at +- Prepare/Execute implemented for Interbase/Firebird +- Changed some regular expressions to be anchored by /^ $/ for speed. +- Added UnixTimeStamp() and UnixDate() to ADOConnection(). Now these functions are in both ADOConnection and ADORecordSet classes. +- Empty recordsets were not cached - fixed. +- Thanks to Gaetano Giunta (g.giunta#libero.it) for the oci8 code review. We didn't agree on everything, but i hoped we agreed to disagree! + +## 1.90 - 6 April 2002 + +- Now all database drivers support fetch modes ADODB_FETCH_NUM and ADODB_FETCH_ASSOC, though still not fully tested. Eg. Frontbase, Sybase, Informix. +- NextRecordSet() support for mssql. Contributed by "Sven Axelsson" sven.axelsson#bokochwebb.se +- Added blob support for SQL Anywhere. Contributed by Wade Johnson wade#wadejohnson.de +- Fixed some security loopholes in server.php. Server.php also supports fetch mode. +- Generalized GenID() to support odbc and mssql drivers. Mssql no longer generates GUID's. +- Experimental RowLock($table,$where) for mssql. +- Properly implemented Prepare() in oci8 and ODBC. +- Added Bind() support to oci8 to support Prepare(). +- Improved error handler. Catches CacheExecute() and GenID() errors now. +- Now if you are running php from the command line, debugging messages do not output html formating. Not 100% complete, but getting there. + +## 1.81 - 22 March 2002 + +- Restored default $ADODB_FETCH_MODE = ADODB_FETCH_DEFAULT for backward compatibility. +- SelectLimit for oci8 improved - Our FIRST_ROWS optimization now does not overwrite existing hint. +- New Sybase SQL Anywhere driver. Contributed by Wade Johnson wade#wadejohnson.de + +## 1.80 - 15 March 2002 + +- Redesigned directory structure of ADOdb files. Added new driver directory where all database drivers reside. +- Changed caching algorithm to create subdirectories. Now we scale better. +- Informix driver now supports insert_id(). Contribution by "Andrea Pinnisi" pinnisi#sysnet.it +- Added experimental ISO date and FetchField support for informix. +- Fixed a quoting bug in Execute() with bind parameters, causing problems with blobs. +- Mssql driver speedup by 10-15%. +- Now in CacheExecute($secs2cache,$sql,...), $secs2cache is optional. If missing, it will take the value defined in $connection->cacheSecs (default is 3600 seconds). Note that CacheSelectLimit(), the secs2cache is still compulsory - sigh. +- Sybase SQL Anywhere driver (using ODBC) contributed by Wade Johnson wade#wadejohnson.de + +## 1.72 - 8 March 2002 + +- Added @ when returning Fields() to prevent spurious error - "Michael William Miller" mille562#pilot.msu.edu +- MetaDatabases() for postgres contributed by Phil pamelant#nerim.net +- Mitchell T. Young (mitch#youngfamily.org) contributed informix driver. +- Fixed rs2html() problem. I cannot reproduce, so probably a problem with pre PHP 4.1.0 versions, when supporting new ADODB_FETCH_MODEs. +- Mattia Rossi (mattia#technologist.com) contributed BlobDecode() and UpdateBlobFile() for postgresql using the postgres specific pg_lo_import()/pg_lo_open() - i don't use them but hopefully others will find this useful. See [this posting](http://phplens.com/lens/lensforum/msgs.php?id=1262) for an example of usage. +- Added UpdateBlobFile() for uploading files to a database. +- Made UpdateBlob() compatible with oci8po driver. +- Added noNullStrings support to oci8 driver. Oracle changes all ' ' strings to nulls, so you need to set strings to ' ' to prevent the nullifying of strings. $conn->noNullStrings = true; will do this for you automatically. This is useful when you define a char column as NOT NULL. +- Fixed UnixTimeStamp() bug - wasn't setting minutes and seconds properly. Patch from Agusti Fita i Borrell agusti#anglatecnic.com. +- Toni Tunkkari added patch for sybase dates. Problem with spaces in day part of date fixed. + +## 1.71 - 18 Jan 2002 + +- Sequence start id support. Now $conn->Gen_ID('seqname', 50) to start sequence from 50. +- CSV driver fix for selectlimit, from Andreas - akaiser#vocote.de. +- Gam3r spotted that a global variable was undefined in the session handler. +- Mssql date regex had error. Fixed - reported by Minh Hoang vb_user#yahoo.com. +- DBTimeStamp() and DBDate() now accept iso dates and unix timestamps. This means that the PostgreSQL handling of dates in GetInsertSQL() and GetUpdateSQL() can be removed. Also if these functions are passed '' or null or false, we return a SQL null. +- GetInsertSQL() and GetUpdateSQL() now accept a new parameter, $magicq to indicate whether quotes should be inserted based on magic quote settings - suggested by dj#4ict.com. +- Reformated docs slightly based on suggestions by Chris Small. + +## 1.65 - 28 Dec 2001 + +- Fixed borland_ibase class naming bug. +- Now instead of using $rs->fields[0] internally, we use reset($rs->fields) so that we are compatible with ADODB_FETCH_ASSOC mode. Reported by Nico S. +- Changed recordset constructor and _initrs() for oci8 so that it returns the field definitions even if no rows in the recordset. Reported by Rick Hickerson (rhickers#mv.mv.com). +- Improved support for postgresql in GetInsertSQL and GetUpdateSQL by "mike" mike#partner2partner.com and "Ryan Bailey" rebel#windriders.com + +## 1.64 - 20 Dec 2001 + +- Danny Milosavljevic added some patches for MySQL error handling and displaying default values. +- Fixed some ADODB_FETCH_BOTH inconsistencies in odbc and interbase. +- Added more tests to test suite to cover ADODB_FETCH_* and ADODB_ERROR_HANDLER. +- Added firebird (ibase) driver +- Added borland_ibase driver for interbase 6.5 + +## 1.63 - 13 Dec 2001 + +- Absolute to the adodb-lib.inc.php file not set properly. Fixed. + +## 1.62 - 11 Dec 2001 + +- Major speedup of ADOdb for low-end web sites by reducing the php code loading and compiling cycle. We conditionally compile not so common functions. Moved csv code to adodb-csvlib.inc.php to reduce adodb.inc.php parsing. This file is loaded only when the csv/proxy driver is used, or CacheExecute() is run. Also moved PageExecute(), GetSelectSQL() and GetUpdateSQL() core code to adodb-lib.inc.php. This reduced the 70K main adodb.inc.php file to 55K, and since at least 20K of the file is comments, we have reduced 50K of code in adodb.inc.php to 35K. There should be 35% reduction in memory and thus 35% speedup in compiling the php code for the main adodb.inc.php file. +- Highly tuned SelectLimit() for oci8 for massive speed improvements on large files. Selecting 20 rows starting from the 20,000th row of a table is now 7 times faster. Thx to Tomas V V Cox. +- Allow . and # in table definitions in GetInsertSQL and GetUpdateSQL. See ADODB_TABLE_REGEX constant. Thx to Ari Kuorikoski. +- Added ADODB_PREFETCH_ROWS constant, defaulting to 10\. This determines the number of records to prefetch in a SELECT statement. Only used by oci8. +- Added high portability Oracle class called oci8po. This uses ? for bind variables, and lower cases column names. +- Now all database drivers support $ADODB_FETCH_MODE, including interbase, ado, and odbc: ADODB_FETCH_NUM and ADODB_FETCH_ASSOC. ADODB_FETCH_BOTH is not fully implemented for all database drivers. + +## 1.61 - Nov 2001 +- Added PO_RecordCount() and PO_Insert_ID(). PO stands for portable. Pablo Roca [pabloroca#mvps.org] +- GenID now returns 0 if not available. Safer is that you should check $conn->hasGenID for availability. +- M'soft ADO we now correctly close recordset in _close() peterd#telephonetics.co.uk +- MSSQL now supports GenID(). It generates a 16-byte GUID from mssql newid() function. +- Changed ereg_replace to preg_replace in SelectLimit. This is a fix for mssql. Ereg doesn't support t or n! Reported by marino Carlos xaplo#postnuke-espanol.org +- Added $recordset->connection. This is the ADOConnection object for the recordset. Works with cached and normal recordsets. Surprisingly, this had no affect on performance! + +## 1.54 - 15 Nov 2001 + +- Fixed some more bugs in PageExecute(). I am getting sick of bug in this and will have to reconsider my QA here. The main issue is that I don't use PageExecute() and to check whether it is working requires a visual inspection of the html generated currently. It is possible to write a test script but it would be quite complicated :( +- More speedups of SelectLimit() for DB2, Oci8, access, vfp, mssql. + +## 1.53 - 7 Nov 2001 + +- Added support for ADODB_FETCH_ASSOC for ado and odbc drivers. +- Tuned GetRowAssoc(false) in postgresql and mysql. +- Stephen Van Dyke contributed ADOdb icon, accepted with some minor mods. +- Enabled Affected_Rows() for postgresql +- Speedup for Concat() using implode() - Benjamin Curtis ben_curtis#yahoo.com +- Fixed some more bugs in PageExecute() to prevent infinite loops + +## 1.52 - 5 Nov 2001 + +- Spelling error in CacheExecute() caused it to fail. $ql should be $sql in line 625! +- Added fixes for parsing [ and ] in GetUpdateSQL(). + +## 1.51 - 5 Nov 2001 + +- Oci8 SelectLimit() speedup by using OCIFetch(). +- Oci8 was mistakenly reporting errors when $db->debug = true. +- If a connection failed with ODBC, it was not correctly reported - fixed. +- _connectionID was inited to -1, changed to false. +- Added $rs->FetchRow(), to simplify API, ala PEAR DB +- Added PEAR DB compat mode, which is still faster than PEAR! See adodb-pear.inc.php. +- Removed postgres pconnect debugging statement. + +## 1.50 - 31 Oct 2001 + +- ADOdbConnection renamed to ADOConnection, and ADOdbFieldObject to ADOFieldObject. +- PageExecute() now checks for empty $rs correctly, and the errors in the docs on this subject have been fixed. +- odbc_error() does not return 6 digit error correctly at times. Implemented workaround. +- Added ADORecordSet_empty class. This will speedup INSERTS/DELETES/UPDATES because the return object created is much smaller. +- Added Prepare() to odbc, and oci8 (but doesn't work properly for oci8 still). +- Made pgsql a synonym for postgre7, and changed SELECT LIMIT to use OFFSET for compat with postgres 7.2. +- Revised adodb-cryptsession.php thanks to Ari. +- Set resources to false on _close, to force freeing of resources. +- Added adodb-errorhandler.inc.php, adodb-errorpear.inc.php and raiseErrorFn on Freek's urging. +- GetRowAssoc($toUpper=true): $toUpper added as default. +- Errors when connecting to a database were not captured formerly. Now we do it correctly. + +## 1.40 - 19 September 2001 + +- PageExecute() to implement page scrolling added. Code and idea by Iván Oliva. +- Some minor postgresql fixes. +- Added sequence support using GenID() for postgresql, oci8, mysql, interbase. +- Added UpdateBlob support for interbase (untested). +- Added encrypted sessions (see adodb-cryptsession.php). By Ari Kuorikoski + +## 1.31 - 21 August 2001 + +- Many bug fixes thanks to "GaM3R (Cameron)" . Some session changes due to Gam3r. +- Fixed qstr() to quote also. +- rs2html() now pretty printed. +- Jonathan Younger contributed the great idea GetUpdateSQL() and GetInsertSQL() which generates SQL to update and insert into a table from a recordset. Modify the recordset fields array, then can this function to generate the SQL (the SQL is not executed). +- Nicola Fankhauser found some bugs in date handling for mssql. +- Added minimal Oracle support for LOBs. Still under development. +- Added $ADODB_FETCH_MODE so you can control whether recordsets return arrays which are numeric, associative or both. This is a global variable you set. Currently only MySQL, Oci8, Postgres drivers support this. +- PostgreSQL properly closes recordsets now. Reported by several people. +- Added UpdateBlob() for Oracle. A hack to make it easier to save blobs. +- Oracle timestamps did not display properly. Fixed. + +## 1.20 - 6 June 2001 + +- Now Oracle can connect using tnsnames.ora or server and service name +- Extensive Oci8 speed optimizations. Oci8 code revised to support variable binding, and /*+ FIRST_ROWS */ hint. +- Worked around some 4.0.6 bugs in odbc_fetch_into(). +- Paolo S. Asioli paolo.asioli#libero.it suggested GetRowAssoc(). +- Escape quotes for oracle wrongly set to '. Now '' is used. +- Variable binding now works in ODBC also. +- Jumped to version 1.20 because I don't like 13 :-) + +## 1.12 - 6 June 2001 + +- Changed $ADODB_DIR to ADODB_DIR constant to plug a security loophole. +- Changed _close() to close persistent connections also. Prevents connection leaks. +- Major revision of oracle and oci8 drivers. Added OCI_RETURN_NULLS and OCI_RETURN_LOBS to OCIFetchInto(). BLOB, CLOB and VARCHAR2 recognition in MetaType() improved. MetaColumns() returns columns in correct sort order. +- Interbase timestamp input format was wrong. Fixed. + +## 1.11 - 20 May 2001 + +- Improved file locking for Windows. +- Probabilistic flushing of cache to avoid avalanche updates when cache timeouts. +- Cached recordset timestamp not saved in some scenarios. Fixed. + +## 1.10 - 19 May 2001 + +- Added caching. CacheExecute() and CacheSelectLimit(). +- Added csv driver. See [http://php.weblogs.com/ADODB_csv](http://php.weblogs.com/adodb_csv). +- Fixed SelectLimit(), SELECT TOP not working under certain circumstances. +- Added better Frontbase support of MetaTypes() by Frank M. Kromann. + +## 1.01 - 24 April 2001 + +- Fixed SelectLimit bug. not quoted properly. +- SelectLimit: SELECT TOP -1 * FROM TABLE not support by Microsoft. Fixed. +- GetMenu improved by glen.davies#cce.ac.nz to support multiple hilited items +- FetchNextObject() did not work with only 1 record returned. Fixed bug reported by $tim#orotech.net +- Fixed mysql field max_length problem. Fix suggested by Jim Nicholson (jnich#att.com) + +## 1.00 - 16 April 2001 + +- Given some brilliant suggestions on how to simplify ADOdb by akul. You no longer need to setup $ADODB_DIR yourself, and ADOLoadCode() is automatically called by ADONewConnection(), simplifying the startup code. +- FetchNextObject() added. Suggested by Jakub Marecek. This makes FetchObject() obsolete, as this is more flexible and powerful. +- Misc fixes to SelectLimit() to support Access (top must follow distinct) and Fields() in the array recordset. From Reinhard Balling. + +## 0.96 - 27 Mar 2001 + +- ADOConnection Close() did not return a value correctly. Thanks to akul#otamedia.com. +- When the horrible magic_quotes is enabled, back-slash () is changed to double-backslash (\). This doesn't make sense for Microsoft/Sybase databases. We fix this in qstr(). +- Fixed Sybase date problem in UnixDate() thanks to Toni Tunkkari. Also fixed MSSQL problem in UnixDate() - thanks to milhouse31#hotmail.com. +- MoveNext() moved to leaf classes for speed in MySQL/PostgreSQL. 10-15% speedup. +- Added null handling in bindInputArray in Execute() -- Ron Baldwin suggestion. +- Fixed some option tags. Thanks to john#jrmstudios.com. + +## 0.95 - 13 Mar 2001 + +- Added postgres7 database driver which supports LIMIT and other version 7 stuff in the future. +- Added SelectLimit to ADOConnection to simulate PostgreSQL's "select * from table limit 10 offset 3". Added helper function GetArrayLimit() to ADORecordSet. +- Fixed mysql metacolumns bug. Thanks to Freek Dijkstra (phpeverywhere#macfreek.com). +- Also many PostgreSQL changes by Freek. He almost rewrote the whole PostgreSQL driver! +- Added fix to input parameters in Execute for non-strings by Ron Baldwin. +- Added new metatype, X for TeXt. Formerly, metatype B for Blob also included text fields. Now 'B' is for binary/image data. 'X' for textual data. +- Fixed $this->GetArray() in GetRows(). +- Oracle and OCI8: 1st parameter is always blank -- now warns if it is filled. +- Now _hasLimit_ and _hasTop_ added to indicate whether SELECT * FROM TABLE LIMIT 10 or SELECT TOP 10 * FROM TABLE are supported. + +## 0.94 - 04 Feb 2001 + +- Added ADORecordSet::GetRows() for compatibility with Microsoft ADO. Synonym for GetArray(). +- Added new metatype 'R' to represent autoincrement numbers. +- Added ADORecordSet.FetchObject() to return a row as an object. +- Finally got a Linux box to test PostgreSql. Many fixes. +- Fixed copyright misspellings in 0.93. +- Fixed mssql MetaColumns type bug. +- Worked around odbc bug in PHP4 for sessions. +- Fixed many documentation bugs (affected_rows, metadatabases, qstr). +- Fixed MySQL timestamp format (removed comma). +- Interbase driver did not call ibase_pconnect(). Fixed. + +## 0.93 - 18 Jan 2002 + +- Fixed GetMenu bug. +- Simplified Interbase commit and rollback. +- Default behaviour on closing a connection is now to rollback all active transactions. +- Added field object handling for array recordset for future XML compatibility. +- Added arr2html() to convert array to html table. + +## 0.92 - 2 Jan 2002 + +- Interbase Commit and Rollback should be working again. +- Changed initialisation of ADORecordSet. This is internal and should not affect users. We are doing this to support cached recordsets in the future. +- Implemented ADORecordSet_array class. This allows you to simulate a database recordset with an array. +- Added UnixDate() and UnixTimeStamp() to ADORecordSet. + +## 0.91 - 21 Dec 2000 + +- Fixed ODBC so ErrorMsg() is working. +- Worked around ADO unrecognised null (0x1) value problem in COM. +- Added Sybase support for FetchField() type +- Removed debugging code and unneeded html from various files +- Changed to javadoc style comments to adodb.inc.php. +- Added maxsql as synonym for mysqlt +- Now ODBC downloads first 8K of blob by default + +## 0.90 - 15 Nov 2000 + +- Lots of testing of Microsoft ADO. Should be more stable now. +- Added $ADODB_COUNTREC. Set to false for high speed selects. +- Added Sybase support. Contributed by Toni Tunkkari (toni.tunkkari#finebyte.com). Bug in Sybase API: GetFields is unable to determine date types. +- Changed behaviour of RecordSet.GetMenu() to support size parameter (listbox) properly. +- Added emptyDate and emptyTimeStamp to RecordSet class that defines how to represent empty dates. +- Added MetaColumns($table) that returns an array of ADOFieldObject's listing the columns of a table. +- Added transaction support for PostgresSQL -- thanks to "Eric G. Werk" egw#netguide.dk. +- Added adodb-session.php for session support. + +## 0.80 - 30 Nov 2000 + +- Added support for charSet for interbase. Implemented MetaTables for most databases. PostgreSQL more extensively tested. + +## 0.71 - 22 Nov 2000 + +- Switched from using require_once to include/include_once for backward compatability with PHP 4.02 and earlier. + +## 0.70 - 15 Nov 2000 + +- Calls by reference have been removed (call_time_pass_reference=Off) to ensure compatibility with future versions of PHP, except in Oracle 7 driver due to a bug in php_oracle.dll. +- PostgreSQL database driver contributed by Alberto Cerezal (acerezalp#dbnet.es). +- Oci8 driver for Oracle 8 contributed by George Fourlanos (fou#infomap.gr). +- Added _mysqlt_ database driver to support MySQL 3.23 which has transaction support. +- Oracle default date format (DD-MON-YY) did not match ADOdb default date format (which is YYYY-MM-DD). Use ALTER SESSION to force the default date. +- Error message checking is now included in test suite. +- MoveNext() did not check EOF properly -- fixed. + +## 0.60 - 8 Nov 2000 + +- Fixed some constructor bugs in ODBC and ADO. Added ErrorNo function to ADOConnection class. + +## 0.51 - 18 Oct 2000 + +- Fixed some interbase bugs. + +## 0.50 - 16 Oct 2000 + +- Interbase commit/rollback changed to be compatible with PHP 4.03\. +- CommitTrans( ) will now return true if transactions not supported. +- Conversely RollbackTrans( ) will return false if transactions not supported. + +## 0.46 - 12 Oct 2000 + +- Many Oracle compatibility issues fixed. + +## 0.40 - 26 Sept 2000 + +- Many bug fixes +- Now Code for BeginTrans, CommitTrans and RollbackTrans is working. So is the Affected_Rows and Insert_ID. Added above functions to test.php. +- ADO type handling was busted in 0.30\. Fixed. +- Generalised Move( ) so it works will all databases, including ODBC. + +## 0.30 - 18 Sept 2000 + +- Renamed ADOLoadDB to ADOLoadCode. This is clearer. +- Added BeginTrans, CommitTrans and RollbackTrans functions. +- Added Affected_Rows() and Insert_ID(), _affectedrows() and _insertID(), ListTables(), ListDatabases(), ListColumns(). +- Need to add New_ID() and hasInsertID and hasAffectedRows, autoCommit + +## 0.20 - 12 Sept 2000 + +- Added support for Microsoft's ADO. +- Added new field to ADORecordSet -- canSeek +- Added new parameter to _fetch($ignore_fields = false). Setting to true will not update fields array for faster performance. +- Added new field to ADORecordSet/ADOConnection -- dataProvider to indicate whether a class is derived from odbc or ado. +- Changed class ODBCFieldObject to ADOFieldObject -- not documented currently. +- Added benchmark.php and testdatabases.inc.php to the test suite. +- Added to ADORecordSet FastForward( ) for future high speed scrolling. Not documented. +- Realised that ADO's Move( ) uses relative positioning. ADOdb uses absolute. + +## 0.10 - 9 Sept 2000 + +- First release diff --git a/app/vendor/adodb/adodb-php/docs/changelog_v3.x.md b/app/vendor/adodb/adodb-php/docs/changelog_v3.x.md new file mode 100644 index 000000000..ee2bded40 --- /dev/null +++ b/app/vendor/adodb/adodb-php/docs/changelog_v3.x.md @@ -0,0 +1,242 @@ +# ADOdb old Changelog - v3.x + +See the [Current Changelog](changelog.md). +Older changelogs: +[v2.x](changelog_v2.x.md). + + +## 3.94 - 11 Oct 2003 + +- Create trigger in datadict-oci8.inc.php did not work, because all cr/lf's must be removed. +- ErrorMsg()/ErrorNo() did not work for many databases when logging enabled. Fixed. +- Removed global variable $ADODB_LOGSQL as it does not work properly with multiple connections. +- Added SQLDate support for sybase. Thx to Chris Phillipson +- Postgresql checking of pgsql resultset resource was incorrect. Fix by Bharat Mediratta bharat#menalto.com. Same patch applied to _insertid and _affectedrows for adodb-postgres64.inc.php. +- Added support for NConnect for postgresql. +- Added Sybase data dict support. Thx to Chris Phillipson +- Extensive improvements in $perf->UI(), eg. Explain now opens in new window, we show scripts which call sql, etc. +- Perf Monitor UI works with magic quotes enabled. +- rsPrefix was declared twice. Removed. +- Oci8 stored procedure support, eg. "begin func(); end;" was incorrect in _query. Fixed. +- Tiraboschi Massimiliano contributed italian language file. +- Fernando Ortiz, fortiz#lacorona.com.mx, contributed informix performance monitor. +- Added _varchar (varchar arrays) support for postgresql. Reported by PREVOT Stéphane. + + +## 3.92 - 22 Sept 2003 + +- Added GetAssoc and CacheGetAssoc to connection object. +- Removed TextMax and CharMax functions from adodb.inc.php. +- HasFailedTrans() returned false when trans failed. Fixed. +- Moved perf driver classes into adodb/perf/*.php. +- Misc improvements to performance monitoring, including UI(). +- RETVAL in mssql Parameter(), we do not append @ now. +- Added Param($name) to connection class, returns '?' or ":$name", for defining bind parameters portably. +- LogSQL traps affected_rows() and saves its value properly now. Also fixed oci8 _stmt and _affectedrows() bugs. +- Session code timestamp check for oci8 works now. Formerly default NLS_DATE_FORMAT stripped off time portion. Thx to Tony Blair (tonanbarbarian#hotmail.com). Also added new $conn->datetime field to oci8, controls whether MetaType() returns 'D' ($this->datetime==false) or 'T' ($this->datetime == true) for DATE type. +- Fixed bugs in adodb-cryptsession.inc.php and adodb-session-clob.inc.php. +- Fixed misc bugs in adodb_key_exists, GetInsertSQL() and GetUpdateSQL(). +- Tuned include_once handling to reduce file-system checking overhead. + +## 3.91 - 9 Sept 2003 + +- Only released to InterAkt +- Added LogSQL() for sql logging and $ADODB_NEWCONNECTION to override factory for driver instantiation. +- Added IfNull($field,$ifNull) function, thx to johnwilk#juno.com +- Added portable substr support. +- Now rs2html() has new parameter, $echo. Set to false to return $html instead of echoing it. + +## 3.90 - 5 Sept 2003 + +- First beta of performance monitoring released. +- MySQL supports MetaTable() masking. +- Fixed key_exists() bug in adodb-lib.inc.php +- Added sp_executesql Prepare() support to mssql. +- Added bind support to db2. +- Added swedish language file - Christian Tiberg" christian#commsoft.nu +- Bug in drop index for mssql data dict fixed. Thx to Gert-Rainer Bitterlich. +- Left join setting for oci8 was wrong. Thx to johnwilk#juno.com + +## 3.80 - 27 Aug 2003 + +- Patch for PHP 4.3.3 cached recordset csv2rs() fread loop incompatibility. +- Added matching mask for MetaTables. Only for oci8, mssql and postgres currently. +- Rewrite of "oracle" driver connection code, merging with "oci8", by Gaetano. +- Added better debugging for Smart Transactions. +- Postgres DBTimeStamp() was wrongly using TO_DATE. Changed to TO_TIMESTAMP. +- ADODB_FETCH_CASE check pushed to ADONewConnection to allow people to define it after including adodb.inc.php. +- Added portugese (brazilian) to languages. Thx to "Levi Fukumori". +- Removed arg3 parameter from Execute/SelectLimit/Cache* functions. +- Execute() now accepts 2-d array as $inputarray. Also changed docs of fnExecute() to note change in sql query counting with 2-d arrays. +- Added MONEY to MetaType in PostgreSQL. +- Added more debugging output to CacheFlush(). + +## 3.72 - 9 Aug 2003 + +- Added qmagic($str), which is a qstr($str) that auto-checks for magic quotes and does the right thing... +- Fixed CacheFlush() bug - Thx to martin#gmx.de +- Walt Boring contributed MetaForeignKeys for postgres7. +- _fetch() called _BlobDecode() wrongly in interbase. Fixed. +- adodb_time bug fixed with dates after 2038 fixed by Jason Pell. http://phplens.com/lens/lensforum/msgs.php?id=6980 + +## 3.71 - 4 Aug 2003 + +- The oci8 driver, MetaPrimaryKeys() did not check the owner correctly when $owner == false. +- Russian language file contributed by "Cyrill Malevanov" cyrill#malevanov.spb.ru. +- Spanish language file contributed by "Horacio Degiorgi" horaciod#codigophp.com. +- Error handling in oci8 bugfix - if there was an error in Execute(), then when calling ErrorNo() and/or ErrorMsg(), the 1st call would return the error, but the 2nd call would return no error. +- Error handling in odbc bugfix. ODBC would always return the last error, even if it happened 5 queries ago. Now we reset the errormsg to '' and errorno to 0 everytime before CacheExecute() and Execute(). + +## 3.70 - 29 July 2003 + +- Added new SQLite driver. Tested on PHP 4.3 and PHP 5. +- Added limited "sapdb" driver support - mainly date support. +- The oci8 driver did not identify NUMBER with no defined precision correctly. +- Added ADODB_FORCE_NULLS, if set, then PHP nulls are converted to SQL nulls in GetInsertSQL/GetUpdateSQL. +- DBDate() and DBTimeStamp() format for postgresql had problems. Fixed. +- Added tableoptions to ChangeTableSQL(). Thx to Mike Benoit. +- Added charset support to postgresql. Thx to Julian Tarkhanov. +- Changed OS check for MS-Windows to prevent confusion with darWIN (MacOS) +- Timestamp format for db2 was wrong. Changed to yyyy-mm-dd-hh.mm.ss.nnnnnn. +- adodb-cryptsession.php includes wrong. Fixed. +- Added MetaForeignKeys(). Supported by mssql, odbc_mssql and oci8. +- Fixed some oci8 MetaColumns/MetaPrimaryKeys bugs. Thx to Walt Boring. +- adodb_getcount() did not init qryRecs to 0\. Missing "WHERE" clause checking in GetUpdateSQL fixed. Thx to Sebastiaan van Stijn. +- Added support for only 'VIEWS' and "TABLES" in MetaTables. From Walt Boring. +- Upgraded to adodb-xmlschema.inc.php 0.0.2. +- NConnect for mysql now returns value. Thx to Dennis Verspuij. +- ADODB_FETCH_BOTH support added to interbase/firebird. +- Czech language file contributed by Kamil Jakubovic jake#host.sk. +- PostgreSQL BlobDecode did not use _connectionID properly. Thx to Juraj Chlebec. +- Added some new initialization stuff for Informix. Thx to "Andrea Pinnisi" pinnisi#sysnet.it +- ADODB_ASSOC_CASE constant wrong in sybase _fetch(). Fixed. + +## 3.60 - 16 June 2003 + +- We now SET CONCAT_NULL_YIELDS_NULL OFF for odbc_mssql driver to be compat with mssql driver. +- The property $emptyDate missing from connection class. Also changed 1903 to constant (TIMESTAMP_FIRST_YEAR=100). Thx to Sebastiaan van Stijn. +- ADOdb speedup optimization - we now return all arrays by reference. +- Now DBDate() and DBTimeStamp() now accepts the string 'null' as a parameter. Suggested by vincent. +- Added GetArray() to connection class. +- Added not_null check in informix metacolumns(). +- Connection parameters for postgresql did not work correctly when port was defined. +- DB2 is now a tested driver, making adodb 100% compatible. Extensive changes to odbc driver for DB2, including implementing serverinfo() and SQLDate(), switching to SQL_CUR_USE_ODBC as the cursor mode, and lastAffectedRows and SelectLimit() fixes. +- The odbc driver's FetchField() field names did not obey ADODB_ASSOC_CASE. Fixed. +- Some bugs in adodb_backtrace() fixed. +- Added "INT IDENTITY" type to adorecordset::MetaType() to support odbc_mssql properly. +- MetaColumns() for oci8, mssql, odbc revised to support scale. Also minor revisions to odbc MetaColumns() for vfp and db2 compat. +- Added unsigned support to mysql datadict class. Thx to iamsure. +- Infinite loop in mssql MoveNext() fixed when ADODB_FETCH_ASSOC used. Thx to Josh R, Night_Wulfe#hotmail.com. +- ChangeTableSQL contributed by Florian Buzin. +- The odbc_mssql driver now sets CONCAT_NULL_YIELDS_NULL OFF for compat with mssql driver. + +## 3.50 - 19 May 2003 + +- Fixed mssql compat with FreeTDS. FreeTDS does not implement mssql_fetch_assoc(). +- Merged back connection and recordset code into adodb.inc.php. +- ADOdb sessions using oracle clobs contributed by achim.gosse#ddd.de. See adodb-session-clob.php. +- Added /s modifier to preg_match everywhere, which ensures that regex does not stop at /n. Thx Pao-Hsi Huang. +- Fixed error in metacolumns() for mssql. +- Added time format support for SQLDate. +- Image => B added to metatype. +- MetaType now checks empty($this->blobSize) instead of empty($this). +- Datadict has beta support for informix, sybase (mapped to mssql), db2 and generic (which is a fudge). +- BlobEncode for postgresql uses pg_escape_bytea, if available. Needed for compat with 7.3. +- Added $ADODB_LANG, to support multiple languages in MetaErrorMsg(). +- Datadict can now parse table definition as declarative text. +- For DataDict, oci8 autoincrement trigger missing semi-colon. Fixed. +- For DataDict, when REPLACE flag enabled, drop sequence in datadict for autoincrement field in postgres and oci8.s +- Postgresql defaults to template1 database if no database defined in connect/pconnect. +- We now clear _resultid in postgresql if query fails. + +## 3.40 - 19 May 2003 + +- Added insert_id for odbc_mssql. +- Modified postgresql UpdateBlobFile() because it did not work in safe mode. +- Now connection object is passed to raiseErrorFn as last parameter. Needed by StartTrans(). +- Added StartTrans() and CompleteTrans(). It is recommended that you do not modify transOff, but use the above functions. +- oci8po now obeys ADODB_ASSOC_CASE settings. +- Added virtualized error codes, using PEAR DB equivalents. Requires you to manually include adodb-error.inc.php yourself, with MetaError() and MetaErrorMsg($errno). +- GetRowAssoc for mysql and pgsql were flawed. Fix by Ross Smith. +- Added to datadict types I1, I2, I4 and I8\. Changed datadict type 'T' to map to timestamp instead of datetime for postgresql. +- Error handling in ExecuteSQLArray(), adodb-datadict.inc.php did not work. +- We now auto-quote postgresql connection parameters when building connection string. +- Added session expiry notification. +- We now test with odbc mysql - made some changes to odbc recordset constructor. +- MetaColumns now special cases access and other databases for odbc. + +## 3.31 - 17 March 2003 + +- Added row checking for _fetch in postgres. +- Added Interval type to MetaType for postgres. +- Remapped postgres driver to call postgres7 driver internally. +- Adorecordset_array::getarray() did not return array when nRows >= 0. +- Postgresql: at times, no error message returned by pg_result_error() but error message returned in pg_last_error(). Recoded again. +- Interbase blob's now use chunking for updateblob. +- Move() did not set EOF correctly. Reported by Jorma T. +- We properly support mysql timestamp fields when we are creating mysql tables using the data-dict interface. +- Table regex includes backticks character now. + +## 3.30 - 3 March 2003 + +- Added $ADODB_EXTENSION and $ADODB_COMPAT_FETCH constant. +- Made blank1stItem configurable using syntax "value:text" in GetMenu/GetMenu2. Thx to Gabriel Birke. +- Previously ADOdb differed from the Microsoft standard because it did not define what to set $this->fields when EOF was reached. Now at EOF, ADOdb sets $this->fields to false for all databases, which is consist with Microsoft's implementation. Postgresql and mysql have always worked this way (in 3.11 and earlier). If you are experiencing compatibility problems (and you are not using postgresql nor mysql) on upgrading to 3.30, try setting the global variables $ADODB_COUNTRECS = true (which is the default) and $ADODB_FETCH_COMPAT = true (this is a new global variable). +- We now check both pg_result_error and pg_last_error as sometimes pg_result_error does not display anything. Iman Mayes +- We no longer check for magic quotes gpc in Quote(). +- Misc fixes for table creation in adodb-datadict.inc.php. Thx to iamsure. +- Time calculations use adodb_time library for all negative timestamps due to problems in Red Hat 7.3 or later. Formerly, only did this for Windows. +- In mssqlpo, we now check if $sql in _query is a string before we change || to +. This is to support prepared stmts. +- Move() and MoveLast() internals changed to support to support EOF and $this->fields change. +- Added ADODB_FETCH_BOTH support to mssql. Thx to Angel Fradejas afradejas#mediafusion.es +- We now check if link resource exists before we run mysql_escape_string in qstr(). +- Before we flock in csv code, we check that it is not a http url. + +## 3.20 - 17 Feb 2003 + +- Added new Data Dictionary classes for creating tables and indexes. Warning - this is very much alpha quality code. The API can still change. See adodb/tests/test-datadict.php for more info. +- We now ignore $ADODB_COUNTRECS for mysql, because PHP truncates incomplete recordsets when mysql_unbuffered_query() is called a second time. +- Now postgresql works correctly when $ADODB_COUNTRECS = false. +- Changed _adodb_getcount to properly support SELECT DISTINCT. +- Discovered that $ADODB_COUNTRECS=true has some problems with prepared queries - suspect PHP bug. +- Now GetOne and GetRow run in $ADODB_COUNTRECS=false mode for better performance. +- Added support for mysql_real_escape_string() and pg_escape_string() in qstr(). +- Added an intermediate variable for mysql _fetch() and MoveNext() to store fields, to prevent overwriting field array with boolean when mysql_fetch_array() returns false. +- Made arrays for getinsertsql and getupdatesql case-insensitive. Suggested by Tim Uckun" tim#diligence.com + +## 3.11 - 11 Feb 2003 + +- Added check for ADODB_NEVER_PERSIST constant in PConnect(). If defined, then PConnect() will actually call non-persistent Connect(). +- Modified interbase to properly work with Prepare(). +- Added $this->ibase_timefmt to allow you to change the date and time format. +- Added support for $input_array parameter in CacheFlush(). +- Added experimental support for dbx, which was then removed when i found that it was slower than using native calls. +- Added MetaPrimaryKeys for mssql and ibase/firebird. +- Added new $trim parameter to GetCol and CacheGetCol +- Uses updated adodb-time.inc.php 0.06. + +## 3.10 - 27 Jan 2003 + +- Added adodb_date(), adodb_getdate(), adodb_mktime() and adodb-time.inc.php. +- For interbase, added code to handle unlimited number of bind parameters. From Daniel Hasan daniel#hasan.cl. +- Added BlobDecode and UpdateBlob for informix. Thx to Fernando Ortiz. +- Added constant ADODB_WINDOWS. If defined, means that running on Windows. +- Added constant ADODB_PHPVER which stores php version as a hex num. Removed $ADODB_PHPVER variable. +- Felho Bacsi reported a minor white-space regular expression problem in GetInsertSQL. +- Modified ADO to use variant to store _affectedRows +- Changed ibase to use base class Replace(). Modified base class Replace() to support ibase. +- Changed odbc to auto-detect when 0 records returned is wrong due to bad odbc drivers. +- Changed mssql to use datetimeconvert ini setting only when 4.30 or later (does not work in 4.23). +- ExecuteCursor($stmt, $cursorname, $params) now accepts a new $params array of additional bind parameters -- William Lovaton walovaton#yahoo.com.mx. +- Added support for sybase_unbuffered_query if ADODB_COUNTRECS == false. Thx to chuck may. +- Fixed FetchNextObj() bug. Thx to Jorma Tuomainen. +- We now use SCOPE_IDENTITY() instead of @@IDENTITY for mssql - thx to marchesini#eside.it +- Changed postgresql movenext logic to prevent illegal row number from being passed to pg_fetch_array(). +- Postgresql initrs bug found by "Bogdan RIPA" bripa#interakt.ro $f1 accidentally named $f + +## 3.00 - 6 Jan 2003 + +- Fixed adodb-pear.inc.php syntax error. +- Improved _adodb_getcount() to use SELECT COUNT(*) FROM ($sql) for languages that accept it. +- Fixed _adodb_getcount() caching error. +- Added sql to retrive table and column info for odbc_mssql. diff --git a/app/vendor/adodb/adodb-php/docs/changelog_v4+5.md b/app/vendor/adodb/adodb-php/docs/changelog_v4+5.md new file mode 100644 index 000000000..c6a9c10de --- /dev/null +++ b/app/vendor/adodb/adodb-php/docs/changelog_v4+5.md @@ -0,0 +1,109 @@ +## 4.990/5.05 - 11 Jul 2008 + +- Added support for multiple recordsets in mysqli Geisel Sierote . See http://phplens.com/lens/lensforum/msgs.php?id=15917 +- Malcolm Cook added new Reload() function to Active Record. See http://phplens.com/lens/lensforum/msgs.php?id=17474 +- Thanks Zoltan Monori [monzol#fotoprizma.hu] for bug fixes in iterator, SelectLimit, GetRandRow, etc. +- Under heavy loads, the performance monitor for oci8 disables Ixora views. +- Fixed sybase driver SQLDate to use str_replace(). Also for adodb5, changed sybase driver UnixDate and UnixTimeStamp calls to static. +- Changed oci8 lob handler to use & reference $this->_refLOBs[$numlob]['VAR'] = &$var. +- We now strtolower the get_class() function in PEAR::isError() for php5 compat. +- CacheExecute did not retrieve cache recordsets properly for 5.04 (worked in 4.98). Fixed. +- New ADODB_Cache_File class for file caching defined in adodb.inc.php. +- Farsi language file contribution by Peyman Hooshmandi Raad (phooshmand#gmail.com) +- New API for creating your custom caching class which is stored in $ADODB_CACHE: + ``` +include "/path/to/adodb.inc.php"; +$ADODB_CACHE_CLASS = 'MyCacheClass'; + +class MyCacheClass extends ADODB_Cache_File +{ + function writecache($filename, $contents,$debug=false){...} + function &readcache($filename, &$err, $secs2cache, $rsClass){ ...} + : +} + +$DB = NewADOConnection($driver); +$DB->Connect(...); ## MyCacheClass created here and stored in $ADODB_CACHE global variable. + +$data = $rs->CacheGetOne($sql); ## MyCacheClass is used here for caching... + ``` +- Memcache supports multiple pooled hosts now. Only if none of the pooled servers can be contacted will a connect error be generated. Usage example below: + ``` +$db = NewADOConnection($driver); +$db->memCache = true; /// should we use memCache instead of caching in files +$db->memCacheHost = array($ip1, $ip2, $ip3); /// $db->memCacheHost = $ip1; still works +$db->memCachePort = 11211; /// this is default memCache port +$db->memCacheCompress = false; /// Use 'true' to store the item compressed (uses zlib) + +$db->Connect(...); +$db->CacheExecute($sql); + ``` + +## 4.98/5.04 - 13 Feb 2008 + +- Fixed adodb_mktime problem which causes a performance bottleneck in $hrs. +- Added mysqli support to adodb_getcount(). +- Removed MYSQLI_TYPE_CHAR from MetaType(). + +## 4.97/5.03 - 22 Jan 2008 + +- Active Record: $ADODB_ASSOC_CASE=1 did not work properly. Fixed. +- Modified Fields() in recordset class to support display null fields in FetchNextObject(). +- In ADOdb5, active record implementation, we now support column names with spaces in them - we autoconvert the spaces to _ using __set(). Thx Daniel Cook. http://phplens.com/lens/lensforum/msgs.php?id=17200 +- Removed $arg3 from mysqli SelectLimit. See http://phplens.com/lens/lensforum/msgs.php?id=16243. Thx Zsolt Szeberenyi. +- Changed oci8 FetchField, which returns the max_length of BLOB/CLOB/NCLOB as 4000 (incorrectly) to -1. +- CacheExecute would sometimes return an error on Windows if it was unable to lock the cache file. This is harmless and has been changed to a warning that can be ignored. Also adodb_write_file() code revised. +- ADOdb perf code changed to only log sql if execution time >= 0.05 seconds. New $ADODB_PERF_MIN variable holds min sql timing. Any SQL with timing value below this and is not causing an error is not logged. +- Also adodb_backtrace() now traces 1 level deeper as sometimes actual culprit function is not displayed. +- Fixed a group by problem with adodb_getcount() for db's which are not postgres/oci8 based. +- Changed mssql driver Parameter() from SQLCHAR to SQLVARCHAR: case 'string': $type = SQLVARCHAR; break. +- Problem with mssql driver in php5 (for adodb 5.03) because some functions are not static. Fixed. + +## 4.96/5.02 - 24 Sept 2007 + +ADOdb perf for oci8 now has non-table-locking code when clearing the sql. Slower but better transparency. Added in 4.96a and 5.02a. +Fix adodb count optimisation. Preg_match did not work properly. Also rewrote the ORDER BY stripping code in _adodb_getcount(), adodb-lib.inc.php. +SelectLimit for oci8 not optimal for large recordsets when offset=0. Changed $nrows check. +Active record optimizations. Added support for assoc arrays in Set(). +Now GetOne returns null if EOF (no records found), and false if error occurs. Use ErrorMsg()/ErrorNo() to get the error. +Also CacheGetRow and CacheGetCol will return false if error occurs, or empty array() if EOF, just like GetRow and GetCol. +Datadict now allows changing of types which are not resizable, eg. VARCHAR to TEXT in ChangeTableSQL. -- Mateo Tibaquirá +Added BIT data type support to adodb-ado.inc.php and adodb-ado5.inc.php. +Ldap driver did not return actual ldap error messages. Fixed. +Implemented GetRandRow($sql, $inputarr). Optimized for Oci8. +Changed adodb5 active record to use static SetDatabaseAdapter() and removed php4 constructor. Bas van Beek bas.vanbeek#gmail.com. +Also in adodb5, changed adodb-session2 to use static function declarations in class. Thx Daniel Berlin. +Added "Clear SQL Log" to bottom of Performance screen. +Sessions2 code echo'ed directly to the screen in debug mode. Now uses ADOConnection::outp(). +In mysql/mysqli, qstr(null) will return the string "null" instead of empty quoted string "''". +postgresql optimizeTable in perf-postgres.inc.php added by Daniel Berlin (mail#daniel-berlin.de) +Added 5.2.1 compat code for oci8. +Changed @@identity to SCOPE_IDENTITY() for multiple mssql drivers. Thx Stefano Nari. +Code sanitization introduced in 4.95 caused problems in European locales (as float 3.2 was typecast to 3,2). Now we only sanitize if is_numeric fails. +Added support for customizing ADORecordset_empty using $this->rsPrefix.'empty'. By Josh Truwin. +Added proper support for ALterColumnSQL for Postgresql in datadict code. Thx. Josh Truwin. +Added better support for MetaType() in mysqli when using an array recordset. +Changed parser for pgsql error messages in adodb-error.inc.php to case-insensitive regex. + +## 4.95/5.01 - 17 May 2007 + +CacheFlush debug outp() passed in invalid parameters. Fixed. +Added Thai language file for adodb. Thx Trirat Petchsingh rosskouk#gmail.com +and Marcos Pont +Added zerofill checking support to MetaColumns for mysql and mysqli. +CacheFlush no longer deletes all files/directories. Only *.cache files +deleted. +DB2 timestamp format changed to var $fmtTimeStamp = +"'Y-m-d-H:i:s'"; +Added some code sanitization to AutoExecute in adodb-lib.inc.php. +Due to typo, all connections in adodb-oracle.inc.php would become +persistent, even non-persistent ones. Fixed. +Oci8 DBTimeStamp uses 24 hour time for input now, so you can perform string +comparisons between 2 DBTimeStamp values. +Some PHP4.4 compat issues fixed in adodb-session2.inc.php +For ADOdb 5.01, fixed some adodb-datadict.inc.php MetaType compat issues +with PHP5. +The $argHostname was wiped out in adodb-ado5.inc.php. Fixed. +Adodb5 version, added iterator support for adodb_recordset_empty. +Adodb5 version,more error checking code now will use exceptions if +available. diff --git a/app/vendor/adodb/adodb-php/docs/changelog_v4.x.md b/app/vendor/adodb/adodb-php/docs/changelog_v4.x.md new file mode 100644 index 000000000..84bdd8a70 --- /dev/null +++ b/app/vendor/adodb/adodb-php/docs/changelog_v4.x.md @@ -0,0 +1,722 @@ +# ADOdb old Changelog - v4.x + +See the [Current Changelog](changelog.md). +Older changelogs: +[v3.x](changelog_v3.x.md), +[v2.x](changelog_v2.x.md). + + +## 4.990 - 11 Jul 2008 + +- Added support for multiple recordsets in mysqli Geisel Sierote . See http://phplens.com/lens/lensforum/msgs.php?id=15917 +- Malcolm Cook added new Reload() function to Active Record. See http://phplens.com/lens/lensforum/msgs.php?id=17474 +- Thanks Zoltan Monori [monzol#fotoprizma.hu] for bug fixes in iterator, SelectLimit, GetRandRow, etc. +- Under heavy loads, the performance monitor for oci8 disables Ixora views. +- Fixed sybase driver SQLDate to use str_replace(). Also for adodb5, changed sybase driver UnixDate and UnixTimeStamp calls to static. +- Changed oci8 lob handler to use & reference $this->_refLOBs[$numlob]['VAR'] = &$var. +- We now strtolower the get_class() function in PEAR::isError() for php5 compat. +- CacheExecute did not retrieve cache recordsets properly for 5.04 (worked in 4.98). Fixed. +- New ADODB_Cache_File class for file caching defined in adodb.inc.php. +- Farsi language file contribution by Peyman Hooshmandi Raad (phooshmand#gmail.com) +- New API for creating your custom caching class which is stored in $ADODB_CACHE: + ``` +include "/path/to/adodb.inc.php"; +$ADODB_CACHE_CLASS = 'MyCacheClass'; + +class MyCacheClass extends ADODB_Cache_File +{ + function writecache($filename, $contents,$debug=false){...} + function &readcache($filename, &$err, $secs2cache, $rsClass){ ...} + : +} + +$DB = NewADOConnection($driver); +$DB->Connect(...); ## MyCacheClass created here and stored in $ADODB_CACHE global variable. + +$data = $rs->CacheGetOne($sql); ## MyCacheClass is used here for caching... +``` +- Memcache supports multiple pooled hosts now. Only if none of the pooled servers can be contacted will a connect error be generated. Usage example below: + ``` +$db = NewADOConnection($driver); +$db->memCache = true; /// should we use memCache instead of caching in files +$db->memCacheHost = array($ip1, $ip2, $ip3); /// $db->memCacheHost = $ip1; still works +$db->memCachePort = 11211; /// this is default memCache port +$db->memCacheCompress = false; /// Use 'true' to store the item compressed (uses zlib) + +$db->Connect(...); +$db->CacheExecute($sql); +``` + +## 4.98 - 13 Feb 2008 + +- Fixed adodb_mktime problem which causes a performance bottleneck in $hrs. +- Added mysqli support to adodb_getcount(). +- Removed MYSQLI_TYPE_CHAR from MetaType(). + +## 4.97 - 22 Jan 2008 + +- Active Record: $ADODB_ASSOC_CASE=1 did not work properly. Fixed. +- Modified Fields() in recordset class to support display null fields in FetchNextObject(). +- In ADOdb5, active record implementation, we now support column names with spaces in them - we autoconvert the spaces to `_` using `__set()`. Thx Daniel Cook. http://phplens.com/lens/lensforum/msgs.php?id=17200 +- Removed $arg3 from mysqli SelectLimit. See http://phplens.com/lens/lensforum/msgs.php?id=16243. Thx Zsolt Szeberenyi. +- Changed oci8 FetchField, which returns the max_length of BLOB/CLOB/NCLOB as 4000 (incorrectly) to -1. +- CacheExecute would sometimes return an error on Windows if it was unable to lock the cache file. This is harmless and has been changed to a warning that can be ignored. Also adodb_write_file() code revised. +- ADOdb perf code changed to only log sql if execution time >= 0.05 seconds. New $ADODB_PERF_MIN variable holds min sql timing. Any SQL with timing value below this and is not causing an error is not logged. +- Also adodb_backtrace() now traces 1 level deeper as sometimes actual culprit function is not displayed. +- Fixed a group by problem with adodb_getcount() for db's which are not postgres/oci8 based. +- Changed mssql driver Parameter() from SQLCHAR to SQLVARCHAR: case 'string': $type = SQLVARCHAR; break. +- Problem with mssql driver in php5 (for adodb 5.03) because some functions are not static. Fixed. + +## 4.96 - 24 Sept 2007 + +- ADOdb perf for oci8 now has non-table-locking code when clearing the sql. Slower but better transparency. Added in 4.96a and 5.02a. +- Fix adodb count optimisation. Preg_match did not work properly. Also rewrote the ORDER BY stripping code in _adodb_getcount(), adodb-lib.inc.php. +- SelectLimit for oci8 not optimal for large recordsets when offset=0. Changed $nrows check. +- Active record optimizations. Added support for assoc arrays in Set(). +- Now GetOne returns null if EOF (no records found), and false if error occurs. Use ErrorMsg()/ErrorNo() to get the error. +- Also CacheGetRow and CacheGetCol will return false if error occurs, or empty array() if EOF, just like GetRow and GetCol. +- Datadict now allows changing of types which are not resizable, eg. VARCHAR to TEXT in ChangeTableSQL. -- Mateo Tibaquirá +- Added BIT data type support to adodb-ado.inc.php and adodb-ado5.inc.php. +- Ldap driver did not return actual ldap error messages. Fixed. +- Implemented GetRandRow($sql, $inputarr). Optimized for Oci8. +- Changed adodb5 active record to use static SetDatabaseAdapter() and removed php4 constructor. Bas van Beek bas.vanbeek#gmail.com. +- Also in adodb5, changed adodb-session2 to use static function declarations in class. Thx Daniel Berlin. +- Added "Clear SQL Log" to bottom of Performance screen. +- Sessions2 code echo'ed directly to the screen in debug mode. Now uses ADOConnection::outp(). +- In mysql/mysqli, qstr(null) will return the string "null" instead of empty quoted string "''". +- postgresql optimizeTable in perf-postgres.inc.php added by Daniel Berlin (mail#daniel-berlin.de) +- Added 5.2.1 compat code for oci8. +- Changed @@identity to SCOPE_IDENTITY() for multiple mssql drivers. Thx Stefano Nari. +- Code sanitization introduced in 4.95 caused problems in European locales (as float 3.2 was typecast to 3,2). Now we only sanitize if is_numeric fails. +- Added support for customizing ADORecordset_empty using $this->rsPrefix.'empty'. By Josh Truwin. +- Added proper support for ALterColumnSQL for Postgresql in datadict code. Thx. Josh Truwin. +- Added better support for MetaType() in mysqli when using an array recordset. +- Changed parser for pgsql error messages in adodb-error.inc.php to case-insensitive regex. + +## 4.95 - 17 May 2007 + +- CacheFlush debug outp() passed in invalid parameters. Fixed. +- Added Thai language file for adodb. Thx Trirat Petchsingh rosskouk#gmail.com and Marcos Pont +- Added zerofill checking support to MetaColumns for mysql and mysqli. +- CacheFlush no longer deletes all files directories. Only `*.cache` files deleted. +- DB2 timestamp format changed to var $fmtTimeStamp = 'Y-m-d-H:i:s'; +- Added some code sanitization to AutoExecute in adodb-lib.inc.php. +- Due to typo, all connections in adodb-oracle.inc.php would become persistent, even non-persistent ones. Fixed. +- Oci8 DBTimeStamp uses 24 hour time for input now, so you can perform string comparisons between 2 DBTimeStamp values. +- Some PHP4.4 compat issues fixed in adodb-session2.inc.php +- For ADOdb 5.01, fixed some adodb-datadict.inc.php MetaType compat issues with PHP5. +- The $argHostname was wiped out in adodb-ado5.inc.php. Fixed. +- Adodb5 version, added iterator support for adodb_recordset_empty. +- Adodb5 version,more error checking code now will use exceptions if available. + +## 4.94 - 23 Jan 2007 + +- Active Record: $ADODB_ASSOC_CASE=2 did not work properly. Fixed. Thx gmane#auxbuss.com. +- mysqli had bugs in BeginTrans() and EndTrans(). Fixed. +- Improved error handling when no database is connected for oci8. Thx Andy Hassall. +- Names longer than 30 chars in oci8 datadict will be changed to random name. Thx Eugenio. http://phplens.com/lens/lensforum/msgs.php?id=16182 +- Added var $upperCase = 'ucase' to access and ado_access drivers. Thx Renato De Giovanni renato#cria.org.br +- Postgres64 driver, if preparing plan failed in _query, did not handle error properly. Fixed. See http://phplens.com/lens/lensforum/msgs.php?id=16131. +- Fixed GetActiveRecordsClass() reference bug. See http://phplens.com/lens/lensforum/msgs.php?id=16120 +- Added handling of nulls in adodb-ado_mssql.inc.php for qstr(). Thx to Felix Rabinovich. +- Adodb-dict contributions by Gaetano + - Support for INDEX in data-dict. Example: idx_ev1. The ability to define indexes using the INDEX keyword was added in ADOdb 4.94. The following example features mutiple indexes, including a compound index idx_ev1. + + ``` + event_id I(11) NOTNULL AUTOINCREMENT PRIMARY, + event_type I(4) NOTNULL + event_start_date T DEFAULT NULL **INDEX id_esd**, + event_end_date T DEFAULT '0000-00-00 00:00:00' **INDEX id_eted**, + event_parent I(11) UNSIGNED NOTNULL DEFAULT 0 **INDEX id_evp**, + event_owner I(11) DEFAULT 0 **INDEX idx_ev1**, + event_project I(11) DEFAULT 0 **INDEX idx_ev1**, + event_times_recuring I(11) UNSIGNED NOTNULL DEFAULT 0, + event_icon C(20) DEFAULT 'obj/event', + event_description X + ``` + + - Prevents the generated SQL from including double drop-sequence statements for REPLACE case of tables with autoincrement columns (on those dbs that emulate it via sequences) + - makes any date defined as DEFAULT value for D and T columns work cross-database, not just the "sysdate" value (as long as it is specified using adodb standard format). See above example. +- Fixed pdo's GetInsertID() support. Thx Ricky Su. +- oci8 Prepare() now sets error messages if an error occurs. +- Added 'PT_BR' to SetDateLocale() -- brazilian portugese. +- charset in oci8 was not set correctly on `*Connect()` +- ADOConnection::Transpose() now appends as first column the field names. +- Added $ADODB_QUOTE_FIELDNAMES. If set to true, will autoquote field names in AutoExecute(),GetInsertSQL(), GetUpdateSQL(). +- Transpose now adds the field names as the first column after transposition. +- Added === check in ADODB_SetDatabaseAdapter for $db, adodb-active-record.inc.php. Thx Christian Affolter. +- Added ErrorNo() to adodb-active-record.inc.php. Thx ante#novisplet.com. + +## 4.93 - 10 Oct 2006 + +- Added support for multiple database connections in performance monitoring code (adodb-perf.inc.php). Now all sql in multiple database connections can be saved into one database ($ADODB_LOG_CONN). +- Added MetaIndexes() to odbc_mssql. +- Added connection property $db->null2null = 'null'. In autoexecute/getinsertsql/getupdatesql, this value will be converted to a null. Set this to a funny invalid value if you do not want null conversion. See http://phplens.com/lens/lensforum/msgs.php?id=15902. +- Path disclosure problem in mysqli fixed. Thx Andy. +- Fixed typo in session_schema2.xml. +- Changed INT in oci8 to return correct precision in $fld->max_length, MetaColumns(). Thx Eloy Lafuente Plaza. +- Patched postgres64 _connect to handle serverinfo(). see http://phplens.com/lens/lensforum/msgs.php?id=15887. +- Added pdo fix for null columns. See http://phplens.com/lens/lensforum/msgs.php?id=15889 +- For stored procedures, missing connection id now passed into mssql_query(). Thx Ecsy (ecsy#freemail.hu). + +## 4.92a - 30 Aug 2006 + +- Syntax error in postgres7 driver. Thx Eloy Lafuente Plaza. +- Minor bug fixes - adodb informix 10 types added to adodb.inc.php. Thx Fernando Ortiz. + +## 4.92 - 29 Aug 2006 + +- Better odbtp date support. +- Added IgnoreErrors() to bypass default error handling. +- The _adodb_getcount() function in adodb-lib.inc.php, some ORDER BY bug fixes. +- For ibase and firebird, set $sysTimeStamp = "CURRENT_TIMESTAMP". +- Fixed postgres connection bug: http://phplens.com/lens/lensforum/msgs.php?id=11057. +- Changed CacheSelectLimit() to flush cache when $secs2cache==-1 due to complaints from other users. +- Added support for using memcached with CacheExecute/CacheSelectLimit. Requires memcache module PECL extension. Usage: + ``` +$db = NewADOConnection($driver); +$db->memCache = true; /// should we use memCache instead of caching in files +$db->memCacheHost = "126.0.1.1"; /// memCache host +$db->memCachePort = 11211; /// this is default memCache port +$db->memCacheCompress = false; /// Use 'true' to store the item compressed (uses zlib) +$db->Connect(...); +$db->CacheExecute($sql); +``` + +- Implemented Transpose() for recordsets. Recordset must be retrieved using ADODB_FETCH_NUM. First column becomes the column name. + ``` +$db = NewADOConnection('mysql'); +$db->Connect(...); +$db->SetFetchMode(ADODB_FETCH_NUM); +$rs = $db->Execute('select productname,productid,unitprice from products limit 10'); +$rs2 = $db->Transpose($rs); +rs2html($rs2); +``` + +## 4.91 - 2 Aug 2006 + +- Major session code rewrite... See session docs. +- PDO bindinputarray() was not set properly for MySQL (changed from true to false). +- Changed CacheSelectLimit() to re-cache when $secs2cache==0. This is one way to flush the cache when SelectLimit is called. +- Added to quotes to mysql and mysqli: "SHOW COLUMNS FROM \`%s\`"; +- Removed accidental optgroup handling in GetMenu(). Fixed ibase _BlobDecode for php5 compat, and also mem alloc issues for small blobs, thx salvatori#interia.pl +- Mysql driver OffsetDate() speedup, useful for adodb-sessions. +- Fix for GetAssoc() PHP5 compat. See http://phplens.com/lens/lensforum/msgs.php?id=15425 +- Active Record - If inserting a record and the value of a primary key field is null, then we do not insert that field in as we assume it is an auto-increment field. Needed by mssql. +- Changed postgres7 MetaForeignKeys() see http://phplens.com/lens/lensforum/msgs.php?id=15531 +- DB2 will now return db2_conn_errormsg() when it is a connection error. + +## 4.90 - 8 June 2006 + +- Changed adodb_countrec() in adodb-lib.inc.php to allow LIMIT to be used as a speedup to reduce no of records counted. +- Added support for transaction modes for postgres and oci8 with SetTransactionMode(). These transaction modes affect all subsequent transactions of that connection. +- Thanks to Halmai Csongor for suggestion. +- Removed `$off = $fieldOffset - 1` line in db2 driver, FetchField(). Tx Larry Menard. +- Added support for PHP5 objects as Execute() bind parameters using `__toString` (eg. Simple-XML). Thx Carl-Christian Salvesen. +- Rounding in tohtml.inc.php did not work properly. Fixed. +- MetaIndexes in postgres fails when fields are deleted then added in again because the attnum has gaps in it. See http://sourceforge.net/tracker/index.php?func=detail&aid=1451245&group_id=42718&atid=433976. Fixed. +- MetaForeignkeys in mysql and mysqli did not work when fetchMode==ADODB_FETCH_ASSOC used. Fixed. +- Reference error in AutoExecute() fixed. +- Added macaddr postgres type to MetaType. Maps to 'C'. +- Added to `_connect()` in adodb-ado5.inc.php support for $database and $dataProvider parameters. Thx Larry Menard. +- Added support for sequences in adodb-ado_mssql.inc.php. Thx Larry Menard. +- Added ADODB_SESSION_READONLY. +- Added session expiryref support to crc32 mode, and in LOB code. +- Clear `_errorMsg` in postgres7 driver, so that ErrorMsg() displays properly when no error occurs. +- Added BindDate and BindTimeStamp + +## 4.81 - 3 May 2006 + +- Fixed variable ref errors in adodb-ado5.inc.php in _query(). +- Mysqli setcharset fix using method_exists(). +- The adodb-perf.inc.php CreateLogTable() code now works for user-defined table names. +- Error in ibase_blob_open() fixed. See http://phplens.com/lens/lensforum/msgs.php?id=14997 + +## 4.80 - 8 Mar 2006 + +- Added activerecord support. +- Added mysql `$conn->compat323 = true` if you want MySQL 3.23 compat enabled. Fixes GetOne() Select-Limit problems. +- Added adodb-xmlschema03.inc.php to support XML Schema version 3 and updated adodb-datadict.htm docs. +- Better memory management in Execute. Thx Mike Fedyk. + +## 4.72 - 21 Feb 2006 + +- Added 'new' DSN parameter for NConnect(). +- Pager now sanitizes $PHP_SELF to protect against XSS. Thx to James Bercegay and others. +- ADOConnection::MetaType changed to setup $rs->connection correctly. +- New native DB2 driver contributed by Larry Menard, Dan Scott, Andy Staudacher, Bharat Mediratta. +- The mssql CreateSequence() did not BEGIN TRANSACTION correctly. Fixed. Thx Sean Lee. +- The _adodb_countrecs() function in adodb-lib.inc.php has been revised to handle more ORDER BY variations. + +## 4.71 - 24 Jan 2006 + +- Fixes postgresql security issue related to binary strings. Thx to Andy Staudacher. +- Several DSN bugs found: + 1. Fix bugs in DSN connections introduced in 4.70 when underscores are found in the DSN. + 2. DSN with _ did not work properly in PHP5 (fine in PHP4). Fixed. + 3. Added support for PDO DSN connections in NewADOConnection(), and database parameter in PDO::Connect(). +- The oci8 datetime flag not correctly implemented in ADORecordSet_array. Fixed. +- Added BlobDelete() to postgres, as a counterpoint to UpdateBlobFile(). +- Fixed GetInsertSQL() to support oci8po. +- Fixed qstr() issue with postgresql with \0 in strings. +- Fixed some datadict driver loading issues in _adodb_getdriver(). +- Added register shutdown function session_write_close in adodb-session.inc.php for PHP 5 compat. See http://phplens.com/lens/lensforum/msgs.php?id=14200. + +## 4.70 - 6 Jan 2006 + +- Many fixes from Danila Ulyanov to ibase, oci8, postgres, mssql, odbc_oracle, odbtp, etc drivers. +- Changed usage of binary hint in adodb-session.inc.php for mysql. See http://phplens.com/lens/lensforum/msgs.php?id=14160 +- Fixed invalid variable reference problem in undomq(), adodb-perf.inc.php. +- Fixed http://phplens.com/lens/lensforum/msgs.php?id=14254 in adodb-perf.inc.php, `_DBParameter()` settings of fetchmode was wrong. +- Fixed security issues in server.php and tmssql.php discussed by Andreas Sandblad in a Secunia security advisory. Added `$ACCEPTIP = 127.0.0.1` and changed suggested root password to something more secure. +- Changed pager to close recordset after RenderLayout(). + +## 4.68 - 25 Nov 2005 + +- PHP 5 compat for mysqli. MetaForeignKeys repeated twice and MYSQLI_BINARY_FLAG missing. +- PHP 5.1 support for postgresql bind parameters using ? did not work if >= 10 parameters. Fixed. Thx to Stanislav Shramko. +- Lots of PDO improvements. +- Spelling error fixed in mysql MetaForeignKeys, $associative parameter. + +## 4.67 - 16 Nov 2005 + +- Postgresql not_null flag not set to false correctly. Thx Cristian MARIN. +- We now check in Replace() if key is in fieldArray. Thx Sébastien Vanvelthem. +- `_file_get_contents()` function was missing in xmlschema. fixed. +- Added week in year support to SQLDate(), using 'W' flag. Thx Spider. +- In sqlite metacolumns was repeated twice, causing PHP 5 problems. Fixed. +- Made debug output XHTML compliant. + +## 4.66 - 28 Sept 2005 + +- ExecuteCursor() in oci8 did not clean up properly on failure. Fixed. +- Updated xmlschema.dtd, by "Alec Smecher" asmecher#smecher.bc.ca +- Hardened SelectLimit, typecasting nrows and offset to integer. +- Fixed misc bugs in AutoExecute() and GetInsertSQL(). +- Added $conn->database as the property holding the database name. The older $conn->databaseName is retained for backward compat. +- Changed _adodb_backtrace() compat check to use function_exists(). +- Bug in postgresql MetaIndexes fixed. Thx Kevin Jamieson. +- Improved OffsetDate for MySQL, reducing rounding error. +- Metacolumns added to sqlite. Thx Mark Newnham. +- PHP 4.4 compat fixes for GetAssoc(). +- Added postgresql bind support for php 5.1. Thx Cristiano da Cunha Duarte +- OffsetDate() fixes for postgresql, typecasting strings to date or timestamp. +- DBTimeStamp formats for mssql, odbc_mssql and postgresql made to conform with other db's. +- Changed PDO constants from PDO_ to PDO:: to support latest spec. + +## 4.65 - 22 July 2005 + +- Reverted 'X' in mssql datadict to 'TEXT' to be compat with mssql driver. However now you can set $datadict->typeX = 'varchar(4000)' or 'TEXT' or 'CLOB' for mssql and oci8 drivers. +- Added charset support when using DSN for Oracle. +- _adodb_getmenu did not use fieldcount() to get number of fields. Fixed. +- MetaForeignKeys() for mysql/mysqli contributed by Juan Carlos Gonzalez. +- MetaDatabases() now correctly returns an array for mysqli driver. Thx Cristian MARIN. +- CompleteTrans(false) did not return false. Fixed. Thx to JMF. +- AutoExecute() did not work with Oracle. Fixed. Thx José Moreira. +- MetaType() added to connection object. +- More PHP 4.4 reference return fixes. Thx Ryan C Bonham and others. + +## 4.64 - 20 June 2005 + +- In datadict, if the default field value is set to '', then it is not applied when the field is created. Fixed by Eugenio. +- MetaPrimaryKeys for postgres did not work because of true/false change in 4.63. Fixed. +- Tested ocifetchstatement in oci8. Rejected at the end. +- Added port to dsn handling. Supported in postgres, mysql, mysqli,ldap. +- Added 'w' and 'l' to mysqli SQLDate(). +- Fixed error handling in ldap _connect() to be more consistent. Also added ErrorMsg() handling to ldap. +- Added support for union in _adodb_getcount, adodb-lib.inc.php for postgres and oci8. +- rs2html() did not work with null dates properly. +- PHP 4.4 reference return fixes. + +## 4.63 - 18 May 2005 + +- Added $nrows<0 check to mysqli's SelectLimit(). +- Added OptimizeTable() and OptimizeTables() in adodb-perf.inc.php. By Markus Staab. +- PostgreSQL inconsistencies fixed. true and false set to TRUE and FALSE, and boolean type in datadict-postgres.inc.php set to 'L' => 'BOOLEAN'. Thx Kevin Jamieson. +- New adodb_session_create_table() function in adodb-session.inc.php. By Markus Staab. +- Added null check to UserTimeStamp(). +- Fixed typo in mysqlt driver in adorecordset. Thx to Andy Staudacher. +- GenID() had a bug in the raiseErrorFn handling. Fixed. Thx Marcos Pont. +- Datadict name quoting now handles ( ) in index fields correctly - they aren't part of the index field. +- Performance monitoring: + 1. oci8 Ixora checks moved down; + 2. expensive sql changed so that only those sql with count(*)>1 are shown; + 3. changed sql1 field to a length+crc32 checksum - this breaks backward compat. +- We remap firebird15 to firebird in data dictionary. + +## 4.62 - 2 Apr 2005 + +- Added 'w' (dow as 0-6 or 1-7) and 'l' (dow as string) for SQLDate for oci8, postgres and mysql. +- Rolled back MetaType() changes for mysqli done in prev version. +- Datadict change by chris, cblin#tennaxia.com data mappings from: + + ``` +oci8: X->varchar(4000) XL->CLOB +mssql: X->XL->TEXT +mysql: X->XL->LONGTEXT +fbird: X->XL->varchar(4000) +``` + to: + ``` +oci8: X->varchar(4000) XL->CLOB +mssql: X->VARCHAR(4000) XL->TEXT +mysql: X->TEXT XL->LONGTEXT +fbird: X->VARCHAR(4000) XL->VARCHAR(32000) +``` +- Added $connection->disableBlobs to postgresql to improve performance when no bytea is used (2-5% improvement). +- Removed all HTTP_* vars. +- Added $rs->tableName to be set before calling AutoExecute(). +- Alex Rootoff rootoff#pisem.net contributed ukrainian language file. +- Added new mysql_option() support using $conn->optionFlags array. +- Added support for ldap_set_option() using the $LDAP_CONNECT_OPTIONS global variable. Contributed by Josh Eldridge. +- Added LDAP_* constant definitions to ldap. +- Added support for boolean bind variables. We use $conn->false and $conn->true to hold values to set false/true to. +- We now do not close the session connection in adodb-session.inc.php as other objects could be using this connection. +- We now strip off `\0` at end of Ixora SQL strings in $perf->tohtml() for oci8. + +## 4.61 - 23 Feb 2005 + +- MySQLi added support for mysqli_connect_errno() and mysqli_connect_error(). +- Massive improvements to alpha PDO driver. +- Quote string bind parameters logged by performance monitor for easy type checking. Thx Jason Judge. +- Added support for $role when connecting with Interbase/firebird. +- Added support for enum recognition in MetaColumns() mysql and mysqli. Thx Amedeo Petrella. +- The sybase_ase driver contributed by Interakt Online. Thx Cristian Marin cristic#interaktonline.com. +- Removed not_null, has_default, and default_value from ADOFieldObject. +- Sessions code, fixed quoting of keys when handling LOBs in session write() function. +- Sessions code, added adodb_session_regenerate_id(), to reduce risk of session hijacking by changing session cookie dynamically. Thx Joe Li. +- Perf monitor, polling for CPU did not work for PHP 4.3.10 and 5.0.0-5.0.3 due to PHP bugs, so we special case these versions. +- Postgresql, UpdateBlob() added code to handle type==CLOB. + +## 4.60 - 24 Jan 2005 + +- Implemented PEAR DB's autoExecute(). Simplified design because I don't like using constants when strings work fine. +- _rs2serialize will now update $rs->sql and $rs->oldProvider. +- Added autoExecute(). +- Added support for postgres8 driver. Currently just remapped to postgres7 driver. +- Changed oci8 _query(), so that OCIBindByName() sets the length to -1 if element size is > 4000. This provides better support for LONGs. +- Added SetDateLocale() support for netherlands (Nl). +- Spelling error in pivot code ($iff should be $iif). +- mysql insert_id() did not work with mysql 3.x. Fixed. +- `\r\n` not converted to spaces correctly in exporting data. Fixed. +- _nconnect() in mysqli did not return value correctly. Fixed. +- Arne Eckmann contributed danish language file. +- Added clone() support to FetchObject() for PHP5. +- Removed SQL_CUR_USE_ODBC from odbc_mssql. + +## 4.55 - 5 Jan 2005 + +- Found bug in Execute() with bind params for db's that do not support binding natively. +- DropSequence() now correctly uses default parameter. +- Now Execute() ignores locale for floats, so 1.23 is NEVER converted to 1,23. +- SetFetchMode() not properly saved in adodb-perf, suspicious sql and expensive sql. Fixed. +- Added INET to postgresql metatypes. Thx motzel. +- Allow oracle hints to work when counting with _adodb_getcount in adodb-lib.inc.php. Thx Chris Wrye. +- Changed mysql insert_id() to use SELECT LAST_INSERT_ID(). +- If alter col in datadict does not modify col type/size of actual col, then it is removed from alter col code. By Mark Newham. Not perfect as MetaType() !== ActualType(). +- Added handling of view fields in metacolumns() for postgresql. Thx Renato De Giovanni. +- Added to informix MetaPrimaryKeys and MetaColumns fixes for null bit. Thx to Cecilio Albero. +- Removed obsolete connection_timeout() from perf code. +- Added support for arrayClass in adodb-csv.inc.php. +- RSFilter now accepts methods of the form $array($obj, 'methodname'). Thx to blake#near-time.com. +- Changed CacheFlush to `$cmd = 'rm -rf '.$ADODB_CACHE_DIR.'/[0-9a-f][0-9a-f]/';` +- For better cursor concurrency, added code to free ref cursors in oci8 when $rs->Close() is called. Note that CLose() is called internally by the Get* functions too. +- Added IIF support for access when pivoting. Thx Volodia Krupach. +- Added mssql datadict support for timestamp. Thx Alexios. +- Informix pager fix. By Mario Ramirez. +- ADODB_TABLE_REGEX now includes ':'. By Mario Ramirez. +- Mark Newnham contributed MetaIndexes for oci8 and db2. + +## 4.54 - 5 Nov 2004 + +- Now you can set $db->charSet = ?? before doing a Connect() in oci8. +- Added adodbFetchMode to sqlite. +- Perf code, added a string typecast to substr in adodb_log_sql(). +- Postgres: Changed BlobDecode() to use po_loread, added new $maxblobsize parameter, and now it returns the blob instead of sending it to stdout - make sure to mention that as a compat warning. Also added $db->IsOID($oid) function; uses a heuristic, not guaranteed to work 100%. +- Contributed arabic language file by "El-Shamaa, Khaled" k.el-shamaa#cgiar.org +- PHP5 exceptions did not handle @ protocol properly. Fixed. +- Added ifnull handling for postgresql (using coalesce). +- Added metatables() support for Postgresql 8.0 (no longer uses pg_% dictionary tables). +- Improved Sybase ErrorMsg() function. By Gaetano Giunta. +- Improved oci8 SelectLimit() to use Prepare(). By Cristiano Duarte. +- Type-cast $row parameter in ifx_fetch_row() to int. Thx stefan bodgan. +- Ralf becker contributed improvements in postgresql, sapdb, mysql data dictionary handling: + - MySql and Postgres MetaType was reporting every int column which was part of a primary key and unique as serial + - Postgres was not reporting the scale of decimal types + - MaxDB was padding the defaults of none-string types with spaces + - MySql now correctly converts enum columns to varchar +- Ralf also changed Postgresql datadict: + - you cant add NOT NULL columns in postgres in one go, they need to be added as NULL and then altered to NOT NULL + - AlterColumnSQL could not change a varchar column with numbers into an integer column, postgres need an explicit conversation + - a re-created sequence was not set to the correct value, if the name was the old name (no implicit sequence), now always the new name of the implicit sequence is used +- Sergio Strampelli added extra $intoken check to Lens_ParseArgs() in datadict code. + +## 4.53 - 28 Sept 2004 + +- FetchMode cached in recordset is sometimes mapped to native db fetchMode. Normally this does not matter, but when using cached recordsets, we need to switch back to using adodb fetchmode. So we cache this in $rs->adodbFetchMode if it differs from the db's fetchMode. +- For informix we now set canSeek = false driver because stefan bodgan tells me that seeking doesn't work. +- SetDateLocale() never worked till now ;-) Thx david#tomato.it +- Set $_bindInputArray = true in sapdb driver. Required for clob support. +- Fixed some PEAR::DB emulation issues with isError() and isWarning. Thx to Gert-Rainer Bitterlich. +- Empty() used in getupdatesql without strlen() check. Fixed. +- Added unsigned detection to mysql and mysqli drivers. Thx to dan cech. +- Added hungarian language file. Thx to Halászvári Gábor. +- Improved fieldname-type formatting of datadict SQL generated (adding $widespacing parameter to _GenField). +- Datadict oci8 DROP CONSTRAINTS misspelt. Fixed. Thx Mark Newnham. +- Changed odbtp to dynamically change databaseType based on connection, eg. from 'odbtp' to 'odbtp_mssql' when connecting to mssql database. +- In datadict, MySQL I4 was wrongly mapped to MEDIUMINT, which is actually I3. Fixed. +- Fixed mysqli MetaType() recognition. Mysqli returns numeric types unlike mysql extension. Thx Francesco Riosa. +- VFP odbc driver curmode set wrongly, causing problems with memo fields. Fixed. +- Odbc driver did not recognize odbc version 2 driver date types properly. Fixed. Thx Bostjan. +- ChangeTableSQL() fixes to datadict-db2.inc.php by Mark Newnham. +- Perf monitoring with odbc improved. Now we try in perf code to manually set the sysTimeStamp using date() if sysTimeStamp is empty. +- All ADO errors are thrown as exceptions in PHP5. So we added exception handling to ado in PHP5 by creating new adodb-ado5.inc.php driver. +- Added IsConnected(). Returns true if connection object connected. By Luca.Gioppo. +- "Ralf Becker" RalfBecker#digitalROCK.de contributed new sapdb data-dictionary driver and a large patch that implements field and table renaming for oracle, mssql, postgresql, mysql and sapdb. See the new RenameTableSQL() and RenameColumnSQL() functions. +- We now check ExecuteCursor to see if PrepareSP was initially called. +- Changed oci8 datadict to use MODIFY for $dd->alterCol. Thx Mark Newnham. + +## 4.52 - 10 Aug 2004 + +- Bug found in Replace() when performance logging enabled, introduced in ADOdb 4.50. Fixed. +- Replace() checks update stmt. If update stmt fails, we now return immediately. Thx to alex. +- Added support for $ADODB_FORCE_TYPE in GetUpdateSQL/GetInsertSQL. Thx to niko. +- Added ADODB_ASSOC_CASE support to postgres/postgres7 driver. +- Support for DECLARE stmt in oci8. Thx Lochbrunner. + +## 4.51 - 29 July 2004 + +- Added adodb-xmlschema 1.0.2. Thx dan and richard. +- Added new adorecordset_ext_* classes. If ADOdb extension installed for mysql, mysqlt and oci8 (but not oci8po), we use the superfast ADOdb extension code for movenext. +- Added schema support to mssql and odbc_mssql MetaPrimaryKeys(). +- Patched MSSQL driver to support PHP NULL and Boolean values while binding the input array parameters in the _query() function. By Stephen Farmer. +- Added support for clob's for mssql, UpdateBlob(). Thx to gfran#directa.com.br +- Added normalize support for postgresql (true=lowercase table name, or false=case-sensitive table names) to MetaColumns($table, $normalize=true). +- PHP5 variant dates in ADO not working. Fixed in adodb-ado.inc.php. +- Constant ADODB_FORCE_NULLS was not working properly for many releases (for GetUpdateSQL). Fixed. Also GetUpdateSQL strips off ORDER BY now - thx Elieser Leão. +- Perf Monitor for oci8 now dynamically highlights optimizer_* params if too high/low. +- Added dsn support to NewADOConnection/ADONewConnection. +- Fixed out of page bounds bug in _adodb_pageexecute_all_rows() Thx to "Sergio Strampelli" sergio#rir.it +- Speedup of movenext for mysql and oci8 drivers. +- Moved debugging code _adodb_debug_execute() to adodb-lib.inc.php. +- Fixed postgresql bytea detection bug. See http://phplens.com/lens/lensforum/msgs.php?id=9849. +- Fixed ibase datetimestamp typo in PHP5. Thx stefan. +- Removed whitespace at end of odbtp drivers. +- Added db2 metaprimarykeys fix. +- Optimizations to MoveNext() for mysql and oci8. Misc speedups to Get* functions. + +## 4.50 - 6 July 2004 + +- Bumped it to 4.50 to avoid confusion with PHP 4.3.x series. +- Added db2 metatables and metacolumns extensions. +- Added alpha PDO driver. Very buggy, only works with odbc. +- Tested mysqli. Set poorAffectedRows = true. Cleaned up movenext() and _fetch(). +- PageExecute does not work properly with php5 (return val not a variable). Reported Dmytro Sychevsky sych#php.com.ua. Fixed. +- MetaTables() for mysql, $showschema parameter was not backward compatible with older versions of adodb. Fixed. +- Changed mysql GetOne() to work with mysql 3.23 when using with non-select stmts (e.g. SHOW TABLES). +- Changed TRIG_ prefix to a variable in datadict-oci8.inc.php. Thx to Luca.Gioppo#csi.it. +- New to adodb-time code. We allow you to define your own daylights savings function, adodb_daylight_sv for pre-1970 dates. If the function is defined (somewhere in an include), then you can correct for daylights savings. See http://phplens.com/phpeverywhere/node/view/16#daylightsavings for more info. +- New sqlitepo driver. This is because assoc mode does not work like other drivers in sqlite. Namely, when selecting (joining) multiple tables, in assoc mode the table names are included in the assoc keys in the "sqlite" driver. In "sqlitepo" driver, the table names are stripped from the returned column names. When this results in a conflict, the first field get preference. Contributed by Herman Kuiper herman#ozuzo.net +- Added $forcenull parameter to GetInsertSQL/GetUpdateSQL. Idea by Marco Aurelio Silva. +- More XHTML changes for GetMenu. By Jeremy Evans. +- Fixes some ibase date issues. Thx to stefan bogdan. +- Improvements to mysqli driver to support $ADODB_COUNTRECS. +- Fixed adodb-csvlib.inc.php problem when reading stream from socket. We need to poll stream continiously. + +## 4.23 - 16 June 2004 + +- New interbase/firebird fixes thx to Lester Caine. Driver fixes a problem with getting field names in the result array, and corrects a couple of data conversions. Also we default to dialect3 for firebird. Also ibase sysDate property was wrong. Changed to cast as timestamp. +- The datadict driver is set up to give quoted tables and fields as this was the only way round reserved words being used as field names in TikiWiki. TikiPro is tidying that up, and I hope to be able to produce a build of THAT which uses what I consider proper UPPERCASE field and table names. The conversion of TikiWiki to ADOdb helped in that, but until the database is completely tidied up in TikiPro ... +- Modified _gencachename() to include fetchmode in name hash. This means you should clear your cache directory after installing this release as the cache name algorithm has changed. +- Now Cache* functions work in safe mode, because we do not create sub-directories in the $ADODB_CACHE_DIR in safe mode. In non-safe mode we still create sub-directories. Done by modifying _gencachename(). +- Added $gmt parameter (true/false) to UserDate and UserTimeStamp in connection class, to force conversion of input (in local time) to be converted to UTC/GMT. +- Mssql datadict did not support INT types properly (no size param allowed). Added _GetSize() to datadict-mssql.inc.php. +- For borland_ibase, BeginTrans(), changed: + + ``` +$this->_transactionID = $this->_connectionID; +``` + + to + + ``` +$this->_transactionID = ibase_trans($this->ibasetrans, $this->_connectionID); +``` + +- Fixed typo in mysqi_field_seek(). Thx to Sh4dow (sh4dow#php.pl). +- LogSQL did not work with Firebird/Interbase. Fixed. +- Postgres: made errorno() handling more consistent. Thx to Michael Jahn, Michael.Jahn#mailbox.tu-dresden.de. +- Added informix patch to better support metatables, metacolumns by "Cecilio Albero" c-albero#eos-i.com +- Cyril Malevanov contributed patch to oci8 to support passing of LOB parameters: + + ``` +$text = 'test test test'; +$sql = "declare rs clob; begin :rs := lobinout(:sa0); end;"; +$stmt = $conn -> PrepareSP($sql); +$conn -> InParameter($stmt,$text,'sa0', -1, OCI_B_CLOB); +$rs = ''; +$conn -> OutParameter($stmt,$rs,'rs', -1, OCI_B_CLOB); +$conn -> Execute($stmt); +echo "return = ".$rs."
";
+``` + + As he says, the LOBs limitations are: + - use OCINewDescriptor before binding + - if Param is IN, uses save() before each execute. This is done automatically for you. + - if Param is OUT, uses load() after each execute. This is done automatically for you. + - when we bind $var as LOB, we create new descriptor and return it as a Bind Result, so if we want to use OUT parameters, we have to store somewhere &$var to load() data from LOB to it. + - IN OUT params are not working now (should not be a big problem to fix it) + - now mass binding not working too (I've wrote about it before) +- Simplified Connect() and PConnect() error handling. +- When extension not loaded, Connect() and PConnect() will return null. On connect error, the fns will return false. +- CacheGetArray() added to code. +- Added Init() to adorecordset_empty(). +- Changed postgres64 driver, MetaColumns() to not strip off quotes in default value if :: detected (type-casting of default). +- Added test: if (!defined('ADODB_DIR')) die(). Useful to prevent hackers from detecting file paths. +- Changed metaTablesSQL to ignore Postgres 7.4 information schemas (sql_*). +- New polish language file by Grzegorz Pacan +- Added support for UNION in _adodb_getcount(). +- Added security check for ADODB_DIR to limit path disclosure issues. Requested by postnuke team. +- Added better error message support to oracle driver. Thx to Gaetano Giunta. +- Added showSchema support to mysql. +- Bind in oci8 did not handle $name=false properly. Fixed. +- If extension not loaded, Connect(), PConnect(), NConnect() will return null. + +## 4.22 - 15 Apr 2004 + +- Moved docs to own adodb/docs folder. +- Fixed session bug when quoting compressed/encrypted data in Replace(). +- Netezza Driver and LDAP drivers contributed by Josh Eldridge. +- GetMenu now uses rtrim() on values instead of trim(). +- Changed MetaColumnNames to return an associative array, keys being the field names in uppercase. +- Suggested fix to adodb-ado.inc.php affected_rows to support PHP5 variants. Thx to Alexios Fakos. +- Contributed bulgarian language file by Valentin Sheiretsky valio#valio.eu.org. +- Contributed romanian language file by stefan bogdan. +- GetInsertSQL now checks for table name (string) in $rs, and will create a recordset for that table automatically. Contributed by Walt Boring. Also added OCI_B_BLOB in bind on Walt's request - hope it doesn't break anything :-) +- Some minor postgres speedups in `_initrs()`. +- ChangeTableSQL checks now if MetaColumns returns empty. Thx Jason Judge. +- Added ADOConnection::Time(), returns current database time in unix timestamp format, or false. + +## 4.21 - 20 Mar 2004 + +- We no longer in SelectLimit for VFP driver add SELECT TOP X unless an ORDER BY exists. +- Pim Koeman contributed dutch language file adodb-nl.inc.php. +- Rick Hickerson added CLOB support to db2 datadict. +- Added odbtp driver. Thx to "stefan bogdan" sbogdan#rsb.ro. +- Changed PrepareSP() 2nd parameter, $cursor, to default to true (formerly false). Fixes oci8 backward compat problems with OUT params. +- Fixed month calculation error in adodb-time.inc.php. 2102-June-01 appeared as 2102-May-32. +- Updated PHP5 RC1 iterator support. API changed, hasMore() renamed to valid(). +- Changed internal format of serialized cache recordsets. As we store a version number, this should be backward compatible. +- Error handling when driver file not found was flawed in ADOLoadCode(). Fixed. + +## 4.20 - 27 Feb 2004 + +- Updated to AXMLS 1.01. +- MetaForeignKeys for postgres7 modified by Edward Jaramilla, works on pg 7.4. +- Now numbers accepts function calls or sequences for GetInsertSQL/GetUpdateSQL numeric fields. +- Changed quotes of 'delete from $perf_table' to "". Thx Kehui (webmaster#kehui.net) +- Added ServerInfo() for ifx, and putenv trim fix. Thx Fernando Ortiz. +- Added addq(), which is analogous to addslashes(). +- Tested with php5b4. Fix some php5 compat problems with exceptions and sybase. +- Carl-Christian Salvesen added patch to mssql _query to support binds greater than 4000 chars. +- Mike suggested patch to PHP5 exception handler. $errno must be numeric. +- Added double quotes (") to ADODB_TABLE_REGEX. +- For oci8, Prepare(...,$cursor), $cursor's meaning was accidentally inverted in 4.11. This causes problems with ExecuteCursor() too, which calls Prepare() internally. Thx to William Lovaton. +- Now dateHasTime property in connection object renamed to datetime for consistency. This could break bc. +- Csongor Halmai reports that db2 SelectLimit with input array is not working. Fixed.. + +## 4.11 - 27 Jan 2004 + +- Csongor Halmai reports db2 binding not working. Reverted back to emulated binding. +- Dan Cech modifies datadict code. Adds support for DropIndex. Minor cleanups. +- Table misspelt in perf-oci8.inc.php. Changed v$conn_cache_advice to v$db_cache_advice. Reported by Steve W. +- UserTimeStamp and DBTimeStamp did not handle YYYYMMDDHHMMSS format properly. Reported by Mike Muir. Fixed. +- Changed oci8 Prepare(). Does not auto-allocate OCINewCursor automatically, unless 2nd param is set to true. This will break backward compat, if Prepare/Execute is used instead of ExecuteCursor. Reported by Chris Jones. +- Added InParameter() and OutParameter(). Wrapper functions to Parameter(), but nicer because they are self-documenting. +- Added 'R' handling in ActualType() to datadict-mysql.inc.php +- Added ADOConnection::SerializableRS($rs). Returns a recordset that can be serialized in a session. +- Added "Run SQL" to performance UI(). +- Misc spelling corrections in adodb-mysqli.inc.php, adodb-oci8.inc.php and datadict-oci8.inc.php, from Heinz Hombergs. +- MetaIndexes() for ibase contributed by Heinz Hombergs. + +## 4.10 - 12 Jan 2004 + +- Dan Cech contributed extensive changes to data dictionary to support name quoting (with `\``), and drop table/index. +- Informix added cursorType property. Default remains IFX_SCROLL, but you can change to 0 (non-scrollable cursor) for performance. +- Added ADODB_View_PrimaryKeys() for returning view primary keys to MetaPrimaryKeys(). +- Simplified chinese file, adodb-cn.inc.php from cysoft. +- Added check for ctype_alnum in adodb-datadict.inc.php. Thx to Jason Judge. +- Added connection parameter to ibase Prepare(). Fix by Daniel Hassan. +- Added nameQuote for quoting identifiers and names to connection obj. Requested by Jason Judge. Also the data dictionary parser now detects `field name` and generates column names with spaces correctly. +- BOOL type not recognised correctly as L. Fixed. +- Fixed paths in ADODB_DIR for session files, and back-ported it to 4.05 (15 Dec 2003) +- Added Schema to postgresql MetaTables. Thx to col#gear.hu +- Empty postgresql recordsets that had blob fields did not set EOF properly. Fixed. +- CacheSelectLimit internal parameters to SelectLimit were wrong. Thx to Nio. +- Modified adodb_pr() and adodb_backtrace() to support command-line usage (eg. no html). +- Fixed some fr and it lang errors. Thx to Gaetano G. +- Added contrib directory, with adodb rs to xmlrpc convertor by Gaetano G. +- Fixed array recordset bugs when `_skiprow1` is true. Thx to Gaetano G. +- Fixed pivot table code when count is false. + +## 4.05 - 13 Dec 2003 + +- Added MetaIndexes to data-dict code - thx to Dan Cech. +- Rewritten session code by Ross Smith. Moved code to adodb/session directory. +- Added function exists check on connecting to most drivers, so we don't crash with the unknown function error. +- Smart Transactions failed with GenID() when it no seq table has been created because the sql statement fails. Fix by Mark Newnham. +- Added $db->length, which holds name of function that returns strlen. +- Fixed error handling for bad driver in ADONewConnection - passed too few params to error-handler. +- Datadict did not handle types like 16.0 properly in _GetSize. Fixed. +- Oci8 driver SelectLimit() bug &= instead of =& used. Thx to Swen Thümmler. +- Jesse Mullan suggested not flushing outp when output buffering enabled. Due to Apache 2.0 bug. Added. +- MetaTables/MetaColumns return ref bug with PHP5 fixed in adodb-datadict.inc.php. +- New mysqli driver contributed by Arjen de Rijke. Based on adodb 3.40 driver. Then jlim added BeginTrans, CommitTrans, RollbackTrans, IfNull, SQLDate. Also fixed return ref bug. +- $ADODB_FLUSH added, if true then force flush in debugging outp. Default is false. In earlier versions, outp defaulted to flush, which is not compat with apache 2.0. +- Mysql driver's GenID() function did not work when when sql logging is on. Fixed. +- $ADODB_SESSION_TBL not declared as global var. Not available if adodb-session.inc.php included in function. Fixed. +- The input array not passed to Execute() in _adodb_getcount(). Fixed. + +## 4.04 - 13 Nov 2003 + +- Switched back to foreach - faster than list-each. +- Fixed bug in ado driver - wiping out $this->fields with date fields. +- Performance Monitor, View SQL, Explain Plan did not work if strlen($SQL)>max($_GET length). Fixed. +- Performance monitor, oci8 driver added memory sort ratio. +- Added random property, returns SQL to generate a floating point number between 0 and 1; + +## 4.03 - 6 Nov 2003 + +- The path to adodb-php4.inc.php and adodb-iterators.inc.php was not setup properly. +- Patched SQLDate in interbase to support hours/mins/secs. Thx to ari kuorikoski. +- Force autorollback for pgsql persistent connections - apparently pgsql did not autorollback properly before 4.3.4. See http://bugs.php.net/bug.php?id=25404 + +## 4.02 - 5 Nov 2003 + +- Some errors in adodb_error_pg() fixed. Thx to Styve. +- Spurious Insert_ID() error was generated by LogSQL(). Fixed. +- Insert_ID was interfering with Affected_Rows() and Replace() when LogSQL() enabled. Fixed. +- More foreach loops optimized with list/each. +- Null dates not handled properly in ADO driver (it becomes 31 Dec 1969!). +- Heinz Hombergs contributed patches for mysql MetaColumns - adding scale, made interbase MetaColumns work with firebird/interbase, and added lang/adodb-de.inc.php. +- Added INFORMIXSERVER environment variable. +- Added $ADODB_ANSI_PADDING_OFF for interbase/firebird. +- PHP 5 beta 2 compat check. Foreach (Iterator) support. Exceptions support. + +## 4.01 - 23 Oct 2003 + +- Fixed bug in rs2html(), tohtml.inc.php, that generated blank table cells. +- Fixed insert_id() incorrectly generated when logsql() enabled. +- Modified PostgreSQL _fixblobs to use list/each instead of foreach. +- Informix ErrorNo() implemented correctly. +- Modified several places to use list/each, including GetRowAssoc(). +- Added UserTimeStamp() to connection class. +- Added $ADODB_ANSI_PADDING_OFF for oci8po. + +## 4.00 - 20 Oct 2003 + +- Upgraded adodb-xmlschema to 1 Oct 2003 snapshot. +- Fix to rs2html warning message. Thx to Filo. +- Fix for odbc_mssql/mssql SQLDate(), hours was wrong. +- Added MetaColumns and MetaPrimaryKeys for sybase. Thx to Chris Phillipson. +- Added autoquoting to datadict for MySQL and PostgreSQL. Suggestion by Karsten Dambekalns diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-access.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-access.inc.php new file mode 100644 index 000000000..3a5a8edc3 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-access.inc.php @@ -0,0 +1,88 @@ +_connectionID); + $rs = new ADORecordSet_odbc($qid); + $ADODB_FETCH_MODE = $savem; + if (!$rs) return false; + + $rs->_has_stupid_odbc_fetch_api_change = $this->_has_stupid_odbc_fetch_api_change; + + $arr = $rs->GetArray(); + //print_pre($arr); + $arr2 = array(); + for ($i=0; $i < sizeof($arr); $i++) { + if ($arr[$i][2] && $arr[$i][3] != 'SYSTEM TABLE') + $arr2[] = $arr[$i][2]; + } + return $arr2; + }*/ +} + + +class ADORecordSet_access extends ADORecordSet_odbc { + + var $databaseType = "access"; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +}// class +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-ado.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-ado.inc.php new file mode 100644 index 000000000..449cdd005 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-ado.inc.php @@ -0,0 +1,660 @@ +_affectedRows = new VARIANT; + } + + function ServerInfo() + { + if (!empty($this->_connectionID)) $desc = $this->_connectionID->provider; + return array('description' => $desc, 'version' => ''); + } + + function _affectedrows() + { + if (PHP_VERSION >= 5) return $this->_affectedRows; + + return $this->_affectedRows->value; + } + + // you can also pass a connection string like this: + // + // $DB->Connect('USER ID=sa;PASSWORD=pwd;SERVER=mangrove;DATABASE=ai',false,false,'SQLOLEDB'); + function _connect($argHostname, $argUsername, $argPassword, $argProvider= 'MSDASQL') + { + $u = 'UID'; + $p = 'PWD'; + + if (!empty($this->charPage)) + $dbc = new COM('ADODB.Connection',null,$this->charPage); + else + $dbc = new COM('ADODB.Connection'); + + if (! $dbc) return false; + + /* special support if provider is mssql or access */ + if ($argProvider=='mssql') { + $u = 'User Id'; //User parameter name for OLEDB + $p = 'Password'; + $argProvider = "SQLOLEDB"; // SQL Server Provider + + // not yet + //if ($argDatabasename) $argHostname .= ";Initial Catalog=$argDatabasename"; + + //use trusted conection for SQL if username not specified + if (!$argUsername) $argHostname .= ";Trusted_Connection=Yes"; + } else if ($argProvider=='access') + $argProvider = "Microsoft.Jet.OLEDB.4.0"; // Microsoft Jet Provider + + if ($argProvider) $dbc->Provider = $argProvider; + + if ($argUsername) $argHostname .= ";$u=$argUsername"; + if ($argPassword)$argHostname .= ";$p=$argPassword"; + + if ($this->debug) ADOConnection::outp( "Host=".$argHostname."
\n version=$dbc->version"); + // @ added below for php 4.0.1 and earlier + @$dbc->Open((string) $argHostname); + + $this->_connectionID = $dbc; + + $dbc->CursorLocation = $this->_cursor_location; + return $dbc->State > 0; + } + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argProvider='MSDASQL') + { + return $this->_connect($argHostname,$argUsername,$argPassword,$argProvider); + } + +/* + adSchemaCatalogs = 1, + adSchemaCharacterSets = 2, + adSchemaCollations = 3, + adSchemaColumns = 4, + adSchemaCheckConstraints = 5, + adSchemaConstraintColumnUsage = 6, + adSchemaConstraintTableUsage = 7, + adSchemaKeyColumnUsage = 8, + adSchemaReferentialContraints = 9, + adSchemaTableConstraints = 10, + adSchemaColumnsDomainUsage = 11, + adSchemaIndexes = 12, + adSchemaColumnPrivileges = 13, + adSchemaTablePrivileges = 14, + adSchemaUsagePrivileges = 15, + adSchemaProcedures = 16, + adSchemaSchemata = 17, + adSchemaSQLLanguages = 18, + adSchemaStatistics = 19, + adSchemaTables = 20, + adSchemaTranslations = 21, + adSchemaProviderTypes = 22, + adSchemaViews = 23, + adSchemaViewColumnUsage = 24, + adSchemaViewTableUsage = 25, + adSchemaProcedureParameters = 26, + adSchemaForeignKeys = 27, + adSchemaPrimaryKeys = 28, + adSchemaProcedureColumns = 29, + adSchemaDBInfoKeywords = 30, + adSchemaDBInfoLiterals = 31, + adSchemaCubes = 32, + adSchemaDimensions = 33, + adSchemaHierarchies = 34, + adSchemaLevels = 35, + adSchemaMeasures = 36, + adSchemaProperties = 37, + adSchemaMembers = 38 + +*/ + + function MetaTables($ttype = false, $showSchema = false, $mask = false) + { + $arr= array(); + $dbc = $this->_connectionID; + + $adors=@$dbc->OpenSchema(20);//tables + if ($adors){ + $f = $adors->Fields(2);//table/view name + $t = $adors->Fields(3);//table type + while (!$adors->EOF){ + $tt=substr($t->value,0,6); + if ($tt!='SYSTEM' && $tt !='ACCESS') + $arr[]=$f->value; + //print $f->value . ' ' . $t->value.'
'; + $adors->MoveNext(); + } + $adors->Close(); + } + + return $arr; + } + + function MetaColumns($table, $normalize=true) + { + $table = strtoupper($table); + $arr = array(); + $dbc = $this->_connectionID; + + $adors=@$dbc->OpenSchema(4);//tables + + if ($adors){ + $t = $adors->Fields(2);//table/view name + while (!$adors->EOF){ + + + if (strtoupper($t->Value) == $table) { + + $fld = new ADOFieldObject(); + $c = $adors->Fields(3); + $fld->name = $c->Value; + $fld->type = 'CHAR'; // cannot discover type in ADO! + $fld->max_length = -1; + $arr[strtoupper($fld->name)]=$fld; + } + + $adors->MoveNext(); + } + $adors->Close(); + } + $false = false; + return empty($arr) ? $false : $arr; + } + + + + + /* returns queryID or false */ + function _query($sql,$inputarr=false) + { + + $dbc = $this->_connectionID; + $false = false; + + // return rs + if ($inputarr) { + + if (!empty($this->charPage)) + $oCmd = new COM('ADODB.Command',null,$this->charPage); + else + $oCmd = new COM('ADODB.Command'); + $oCmd->ActiveConnection = $dbc; + $oCmd->CommandText = $sql; + $oCmd->CommandType = 1; + + // Map by http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ado270/htm/mdmthcreateparam.asp + // Check issue http://bugs.php.net/bug.php?id=40664 !!! + while(list(, $val) = each($inputarr)) { + $type = gettype($val); + $len=strlen($val); + if ($type == 'boolean') + $this->adoParameterType = 11; + else if ($type == 'integer') + $this->adoParameterType = 3; + else if ($type == 'double') + $this->adoParameterType = 5; + elseif ($type == 'string') + $this->adoParameterType = 202; + else if (($val === null) || (!defined($val))) + $len=1; + else + $this->adoParameterType = 130; + + // name, type, direction 1 = input, len, + $p = $oCmd->CreateParameter('name',$this->adoParameterType,1,$len,$val); + + $oCmd->Parameters->Append($p); + } + $p = false; + $rs = $oCmd->Execute(); + $e = $dbc->Errors; + if ($dbc->Errors->Count > 0) return $false; + return $rs; + } + + $rs = @$dbc->Execute($sql,$this->_affectedRows, $this->_execute_option); + + if ($dbc->Errors->Count > 0) return $false; + if (! $rs) return $false; + + if ($rs->State == 0) { + $true = true; + return $true; // 0 = adStateClosed means no records returned + } + return $rs; + } + + + function BeginTrans() + { + if ($this->transOff) return true; + + if (isset($this->_thisTransactions)) + if (!$this->_thisTransactions) return false; + else { + $o = $this->_connectionID->Properties("Transaction DDL"); + $this->_thisTransactions = $o ? true : false; + if (!$o) return false; + } + @$this->_connectionID->BeginTrans(); + $this->transCnt += 1; + return true; + } + + function CommitTrans($ok=true) + { + if (!$ok) return $this->RollbackTrans(); + if ($this->transOff) return true; + + @$this->_connectionID->CommitTrans(); + if ($this->transCnt) @$this->transCnt -= 1; + return true; + } + function RollbackTrans() { + if ($this->transOff) return true; + @$this->_connectionID->RollbackTrans(); + if ($this->transCnt) @$this->transCnt -= 1; + return true; + } + + /* Returns: the last error message from previous database operation */ + + function ErrorMsg() + { + if (!$this->_connectionID) return "No connection established"; + $errc = $this->_connectionID->Errors; + if (!$errc) return "No Errors object found"; + if ($errc->Count == 0) return ''; + $err = $errc->Item($errc->Count-1); + return $err->Description; + } + + function ErrorNo() + { + $errc = $this->_connectionID->Errors; + if ($errc->Count == 0) return 0; + $err = $errc->Item($errc->Count-1); + return $err->NativeError; + } + + // returns true or false + function _close() + { + if ($this->_connectionID) $this->_connectionID->Close(); + $this->_connectionID = false; + return true; + } + + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordSet_ado extends ADORecordSet { + + var $bind = false; + var $databaseType = "ado"; + var $dataProvider = "ado"; + var $_tarr = false; // caches the types + var $_flds; // and field objects + var $canSeek = true; + var $hideErrors = true; + + function __construct($id,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + $this->fetchMode = $mode; + return parent::__construct($id,$mode); + } + + + // returns the field object + function FetchField($fieldOffset = -1) { + $off=$fieldOffset+1; // offsets begin at 1 + + $o= new ADOFieldObject(); + $rs = $this->_queryID; + $f = $rs->Fields($fieldOffset); + $o->name = $f->Name; + $t = $f->Type; + $o->type = $this->MetaType($t); + $o->max_length = $f->DefinedSize; + $o->ado_type = $t; + + //print "off=$off name=$o->name type=$o->type len=$o->max_length
"; + return $o; + } + + /* Use associative array to get fields array */ + function Fields($colname) + { + if ($this->fetchMode & ADODB_FETCH_ASSOC) return $this->fields[$colname]; + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + + function _initrs() + { + $rs = $this->_queryID; + $this->_numOfRows = $rs->RecordCount; + + $f = $rs->Fields; + $this->_numOfFields = $f->Count; + } + + + // should only be used to move forward as we normally use forward-only cursors + function _seek($row) + { + $rs = $this->_queryID; + // absoluteposition doesn't work -- my maths is wrong ? + // $rs->AbsolutePosition->$row-2; + // return true; + if ($this->_currentRow > $row) return false; + @$rs->Move((integer)$row - $this->_currentRow-1); //adBookmarkFirst + return true; + } + +/* + OLEDB types + + enum DBTYPEENUM + { DBTYPE_EMPTY = 0, + DBTYPE_NULL = 1, + DBTYPE_I2 = 2, + DBTYPE_I4 = 3, + DBTYPE_R4 = 4, + DBTYPE_R8 = 5, + DBTYPE_CY = 6, + DBTYPE_DATE = 7, + DBTYPE_BSTR = 8, + DBTYPE_IDISPATCH = 9, + DBTYPE_ERROR = 10, + DBTYPE_BOOL = 11, + DBTYPE_VARIANT = 12, + DBTYPE_IUNKNOWN = 13, + DBTYPE_DECIMAL = 14, + DBTYPE_UI1 = 17, + DBTYPE_ARRAY = 0x2000, + DBTYPE_BYREF = 0x4000, + DBTYPE_I1 = 16, + DBTYPE_UI2 = 18, + DBTYPE_UI4 = 19, + DBTYPE_I8 = 20, + DBTYPE_UI8 = 21, + DBTYPE_GUID = 72, + DBTYPE_VECTOR = 0x1000, + DBTYPE_RESERVED = 0x8000, + DBTYPE_BYTES = 128, + DBTYPE_STR = 129, + DBTYPE_WSTR = 130, + DBTYPE_NUMERIC = 131, + DBTYPE_UDT = 132, + DBTYPE_DBDATE = 133, + DBTYPE_DBTIME = 134, + DBTYPE_DBTIMESTAMP = 135 + + ADO Types + + adEmpty = 0, + adTinyInt = 16, + adSmallInt = 2, + adInteger = 3, + adBigInt = 20, + adUnsignedTinyInt = 17, + adUnsignedSmallInt = 18, + adUnsignedInt = 19, + adUnsignedBigInt = 21, + adSingle = 4, + adDouble = 5, + adCurrency = 6, + adDecimal = 14, + adNumeric = 131, + adBoolean = 11, + adError = 10, + adUserDefined = 132, + adVariant = 12, + adIDispatch = 9, + adIUnknown = 13, + adGUID = 72, + adDate = 7, + adDBDate = 133, + adDBTime = 134, + adDBTimeStamp = 135, + adBSTR = 8, + adChar = 129, + adVarChar = 200, + adLongVarChar = 201, + adWChar = 130, + adVarWChar = 202, + adLongVarWChar = 203, + adBinary = 128, + adVarBinary = 204, + adLongVarBinary = 205, + adChapter = 136, + adFileTime = 64, + adDBFileTime = 137, + adPropVariant = 138, + adVarNumeric = 139 +*/ + function MetaType($t,$len=-1,$fieldobj=false) + { + if (is_object($t)) { + $fieldobj = $t; + $t = $fieldobj->type; + $len = $fieldobj->max_length; + } + + if (!is_numeric($t)) return $t; + + switch ($t) { + case 0: + case 12: // variant + case 8: // bstr + case 129: //char + case 130: //wc + case 200: // varc + case 202:// varWC + case 128: // bin + case 204: // varBin + case 72: // guid + if ($len <= $this->blobSize) return 'C'; + + case 201: + case 203: + return 'X'; + case 128: + case 204: + case 205: + return 'B'; + case 7: + case 133: return 'D'; + + case 134: + case 135: return 'T'; + + case 11: return 'L'; + + case 16:// adTinyInt = 16, + case 2://adSmallInt = 2, + case 3://adInteger = 3, + case 4://adBigInt = 20, + case 17://adUnsignedTinyInt = 17, + case 18://adUnsignedSmallInt = 18, + case 19://adUnsignedInt = 19, + case 20://adUnsignedBigInt = 21, + return 'I'; + default: return 'N'; + } + } + + // time stamp not supported yet + function _fetch() + { + $rs = $this->_queryID; + if (!$rs or $rs->EOF) { + $this->fields = false; + return false; + } + $this->fields = array(); + + if (!$this->_tarr) { + $tarr = array(); + $flds = array(); + for ($i=0,$max = $this->_numOfFields; $i < $max; $i++) { + $f = $rs->Fields($i); + $flds[] = $f; + $tarr[] = $f->Type; + } + // bind types and flds only once + $this->_tarr = $tarr; + $this->_flds = $flds; + } + $t = reset($this->_tarr); + $f = reset($this->_flds); + + if ($this->hideErrors) $olde = error_reporting(E_ERROR|E_CORE_ERROR);// sometimes $f->value be null + for ($i=0,$max = $this->_numOfFields; $i < $max; $i++) { + //echo "

",$t,' ';var_dump($f->value); echo '

'; + switch($t) { + case 135: // timestamp + if (!strlen((string)$f->value)) $this->fields[] = false; + else { + if (!is_numeric($f->value)) # $val = variant_date_to_timestamp($f->value); + // VT_DATE stores dates as (float) fractional days since 1899/12/30 00:00:00 + $val=(float) variant_cast($f->value,VT_R8)*3600*24-2209161600; + else + $val = $f->value; + $this->fields[] = adodb_date('Y-m-d H:i:s',$val); + } + break; + case 133:// A date value (yyyymmdd) + if ($val = $f->value) { + $this->fields[] = substr($val,0,4).'-'.substr($val,4,2).'-'.substr($val,6,2); + } else + $this->fields[] = false; + break; + case 7: // adDate + if (!strlen((string)$f->value)) $this->fields[] = false; + else { + if (!is_numeric($f->value)) $val = variant_date_to_timestamp($f->value); + else $val = $f->value; + + if (($val % 86400) == 0) $this->fields[] = adodb_date('Y-m-d',$val); + else $this->fields[] = adodb_date('Y-m-d H:i:s',$val); + } + break; + case 1: // null + $this->fields[] = false; + break; + case 6: // currency is not supported properly; + ADOConnection::outp( ''.$f->Name.': currency type not supported by PHP'); + $this->fields[] = (float) $f->value; + break; + case 11: //BIT; + $val = ""; + if(is_bool($f->value)) { + if($f->value==true) $val = 1; + else $val = 0; + } + if(is_null($f->value)) $val = null; + + $this->fields[] = $val; + break; + default: + $this->fields[] = $f->value; + break; + } + //print " $f->value $t, "; + $f = next($this->_flds); + $t = next($this->_tarr); + } // for + if ($this->hideErrors) error_reporting($olde); + @$rs->MoveNext(); // @ needed for some versions of PHP! + + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + $this->fields = $this->GetRowAssoc(); + } + return true; + } + + function NextRecordSet() + { + $rs = $this->_queryID; + $this->_queryID = $rs->NextRecordSet(); + //$this->_queryID = $this->_QueryId->NextRecordSet(); + if ($this->_queryID == null) return false; + + $this->_currentRow = -1; + $this->_currentPage = -1; + $this->bind = false; + $this->fields = false; + $this->_flds = false; + $this->_tarr = false; + + $this->_inited = false; + $this->Init(); + return true; + } + + function _close() { + $this->_flds = false; + @$this->_queryID->Close();// by Pete Dishman (peterd@telephonetics.co.uk) + $this->_queryID = false; + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-ado5.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-ado5.inc.php new file mode 100644 index 000000000..21df32f16 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-ado5.inc.php @@ -0,0 +1,708 @@ +_affectedRows = new VARIANT; + } + + function ServerInfo() + { + if (!empty($this->_connectionID)) $desc = $this->_connectionID->provider; + return array('description' => $desc, 'version' => ''); + } + + function _affectedrows() + { + if (PHP_VERSION >= 5) return $this->_affectedRows; + + return $this->_affectedRows->value; + } + + // you can also pass a connection string like this: + // + // $DB->Connect('USER ID=sa;PASSWORD=pwd;SERVER=mangrove;DATABASE=ai',false,false,'SQLOLEDB'); + function _connect($argHostname, $argUsername, $argPassword,$argDBorProvider, $argProvider= '') + { + // two modes + // - if $argProvider is empty, we assume that $argDBorProvider holds provider -- this is for backward compat + // - if $argProvider is not empty, then $argDBorProvider holds db + + + if ($argProvider) { + $argDatabasename = $argDBorProvider; + } else { + $argDatabasename = ''; + if ($argDBorProvider) $argProvider = $argDBorProvider; + else if (stripos($argHostname,'PROVIDER') === false) /* full conn string is not in $argHostname */ + $argProvider = 'MSDASQL'; + } + + + try { + $u = 'UID'; + $p = 'PWD'; + + if (!empty($this->charPage)) + $dbc = new COM('ADODB.Connection',null,$this->charPage); + else + $dbc = new COM('ADODB.Connection'); + + if (! $dbc) return false; + + /* special support if provider is mssql or access */ + if ($argProvider=='mssql') { + $u = 'User Id'; //User parameter name for OLEDB + $p = 'Password'; + $argProvider = "SQLOLEDB"; // SQL Server Provider + + // not yet + //if ($argDatabasename) $argHostname .= ";Initial Catalog=$argDatabasename"; + + //use trusted conection for SQL if username not specified + if (!$argUsername) $argHostname .= ";Trusted_Connection=Yes"; + } else if ($argProvider=='access') + $argProvider = "Microsoft.Jet.OLEDB.4.0"; // Microsoft Jet Provider + + if ($argProvider) $dbc->Provider = $argProvider; + + if ($argProvider) $argHostname = "PROVIDER=$argProvider;DRIVER={SQL Server};SERVER=$argHostname"; + + + if ($argDatabasename) $argHostname .= ";DATABASE=$argDatabasename"; + if ($argUsername) $argHostname .= ";$u=$argUsername"; + if ($argPassword)$argHostname .= ";$p=$argPassword"; + + if ($this->debug) ADOConnection::outp( "Host=".$argHostname."
\n version=$dbc->version"); + // @ added below for php 4.0.1 and earlier + @$dbc->Open((string) $argHostname); + + $this->_connectionID = $dbc; + + $dbc->CursorLocation = $this->_cursor_location; + return $dbc->State > 0; + } catch (exception $e) { + if ($this->debug) echo "
",$argHostname,"\n",$e,"
\n"; + } + + return false; + } + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argProvider='MSDASQL') + { + return $this->_connect($argHostname,$argUsername,$argPassword,$argProvider); + } + +/* + adSchemaCatalogs = 1, + adSchemaCharacterSets = 2, + adSchemaCollations = 3, + adSchemaColumns = 4, + adSchemaCheckConstraints = 5, + adSchemaConstraintColumnUsage = 6, + adSchemaConstraintTableUsage = 7, + adSchemaKeyColumnUsage = 8, + adSchemaReferentialContraints = 9, + adSchemaTableConstraints = 10, + adSchemaColumnsDomainUsage = 11, + adSchemaIndexes = 12, + adSchemaColumnPrivileges = 13, + adSchemaTablePrivileges = 14, + adSchemaUsagePrivileges = 15, + adSchemaProcedures = 16, + adSchemaSchemata = 17, + adSchemaSQLLanguages = 18, + adSchemaStatistics = 19, + adSchemaTables = 20, + adSchemaTranslations = 21, + adSchemaProviderTypes = 22, + adSchemaViews = 23, + adSchemaViewColumnUsage = 24, + adSchemaViewTableUsage = 25, + adSchemaProcedureParameters = 26, + adSchemaForeignKeys = 27, + adSchemaPrimaryKeys = 28, + adSchemaProcedureColumns = 29, + adSchemaDBInfoKeywords = 30, + adSchemaDBInfoLiterals = 31, + adSchemaCubes = 32, + adSchemaDimensions = 33, + adSchemaHierarchies = 34, + adSchemaLevels = 35, + adSchemaMeasures = 36, + adSchemaProperties = 37, + adSchemaMembers = 38 + +*/ + + function MetaTables($ttype = false, $showSchema = false, $mask = false) + { + $arr= array(); + $dbc = $this->_connectionID; + + $adors=@$dbc->OpenSchema(20);//tables + if ($adors){ + $f = $adors->Fields(2);//table/view name + $t = $adors->Fields(3);//table type + while (!$adors->EOF){ + $tt=substr($t->value,0,6); + if ($tt!='SYSTEM' && $tt !='ACCESS') + $arr[]=$f->value; + //print $f->value . ' ' . $t->value.'
'; + $adors->MoveNext(); + } + $adors->Close(); + } + + return $arr; + } + + function MetaColumns($table, $normalize=true) + { + $table = strtoupper($table); + $arr= array(); + $dbc = $this->_connectionID; + + $adors=@$dbc->OpenSchema(4);//tables + + if ($adors){ + $t = $adors->Fields(2);//table/view name + while (!$adors->EOF){ + + + if (strtoupper($t->Value) == $table) { + + $fld = new ADOFieldObject(); + $c = $adors->Fields(3); + $fld->name = $c->Value; + $fld->type = 'CHAR'; // cannot discover type in ADO! + $fld->max_length = -1; + $arr[strtoupper($fld->name)]=$fld; + } + + $adors->MoveNext(); + } + $adors->Close(); + } + + return $arr; + } + + /* returns queryID or false */ + function _query($sql,$inputarr=false) + { + try { // In PHP5, all COM errors are exceptions, so to maintain old behaviour... + + $dbc = $this->_connectionID; + + // return rs + + $false = false; + + if ($inputarr) { + + if (!empty($this->charPage)) + $oCmd = new COM('ADODB.Command',null,$this->charPage); + else + $oCmd = new COM('ADODB.Command'); + $oCmd->ActiveConnection = $dbc; + $oCmd->CommandText = $sql; + $oCmd->CommandType = 1; + + while(list(, $val) = each($inputarr)) { + $type = gettype($val); + $len=strlen($val); + if ($type == 'boolean') + $this->adoParameterType = 11; + else if ($type == 'integer') + $this->adoParameterType = 3; + else if ($type == 'double') + $this->adoParameterType = 5; + elseif ($type == 'string') + $this->adoParameterType = 202; + else if (($val === null) || (!defined($val))) + $len=1; + else + $this->adoParameterType = 130; + + // name, type, direction 1 = input, len, + $p = $oCmd->CreateParameter('name',$this->adoParameterType,1,$len,$val); + + $oCmd->Parameters->Append($p); + } + + $p = false; + $rs = $oCmd->Execute(); + $e = $dbc->Errors; + if ($dbc->Errors->Count > 0) return $false; + return $rs; + } + + $rs = @$dbc->Execute($sql,$this->_affectedRows, $this->_execute_option); + + if ($dbc->Errors->Count > 0) return $false; + if (! $rs) return $false; + + if ($rs->State == 0) { + $true = true; + return $true; // 0 = adStateClosed means no records returned + } + return $rs; + + } catch (exception $e) { + + } + return $false; + } + + + function BeginTrans() + { + if ($this->transOff) return true; + + if (isset($this->_thisTransactions)) + if (!$this->_thisTransactions) return false; + else { + $o = $this->_connectionID->Properties("Transaction DDL"); + $this->_thisTransactions = $o ? true : false; + if (!$o) return false; + } + @$this->_connectionID->BeginTrans(); + $this->transCnt += 1; + return true; + } + function CommitTrans($ok=true) + { + if (!$ok) return $this->RollbackTrans(); + if ($this->transOff) return true; + + @$this->_connectionID->CommitTrans(); + if ($this->transCnt) @$this->transCnt -= 1; + return true; + } + function RollbackTrans() { + if ($this->transOff) return true; + @$this->_connectionID->RollbackTrans(); + if ($this->transCnt) @$this->transCnt -= 1; + return true; + } + + /* Returns: the last error message from previous database operation */ + + function ErrorMsg() + { + if (!$this->_connectionID) return "No connection established"; + $errmsg = ''; + + try { + $errc = $this->_connectionID->Errors; + if (!$errc) return "No Errors object found"; + if ($errc->Count == 0) return ''; + $err = $errc->Item($errc->Count-1); + $errmsg = $err->Description; + }catch(exception $e) { + } + return $errmsg; + } + + function ErrorNo() + { + $errc = $this->_connectionID->Errors; + if ($errc->Count == 0) return 0; + $err = $errc->Item($errc->Count-1); + return $err->NativeError; + } + + // returns true or false + function _close() + { + if ($this->_connectionID) $this->_connectionID->Close(); + $this->_connectionID = false; + return true; + } + + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordSet_ado extends ADORecordSet { + + var $bind = false; + var $databaseType = "ado"; + var $dataProvider = "ado"; + var $_tarr = false; // caches the types + var $_flds; // and field objects + var $canSeek = true; + var $hideErrors = true; + + function __construct($id,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + $this->fetchMode = $mode; + return parent::__construct($id,$mode); + } + + + // returns the field object + function FetchField($fieldOffset = -1) { + $off=$fieldOffset+1; // offsets begin at 1 + + $o= new ADOFieldObject(); + $rs = $this->_queryID; + if (!$rs) return false; + + $f = $rs->Fields($fieldOffset); + $o->name = $f->Name; + $t = $f->Type; + $o->type = $this->MetaType($t); + $o->max_length = $f->DefinedSize; + $o->ado_type = $t; + + + //print "off=$off name=$o->name type=$o->type len=$o->max_length
"; + return $o; + } + + /* Use associative array to get fields array */ + function Fields($colname) + { + if ($this->fetchMode & ADODB_FETCH_ASSOC) return $this->fields[$colname]; + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + + function _initrs() + { + $rs = $this->_queryID; + + try { + $this->_numOfRows = $rs->RecordCount; + } catch (Exception $e) { + $this->_numOfRows = -1; + } + $f = $rs->Fields; + $this->_numOfFields = $f->Count; + } + + + // should only be used to move forward as we normally use forward-only cursors + function _seek($row) + { + $rs = $this->_queryID; + // absoluteposition doesn't work -- my maths is wrong ? + // $rs->AbsolutePosition->$row-2; + // return true; + if ($this->_currentRow > $row) return false; + @$rs->Move((integer)$row - $this->_currentRow-1); //adBookmarkFirst + return true; + } + +/* + OLEDB types + + enum DBTYPEENUM + { DBTYPE_EMPTY = 0, + DBTYPE_NULL = 1, + DBTYPE_I2 = 2, + DBTYPE_I4 = 3, + DBTYPE_R4 = 4, + DBTYPE_R8 = 5, + DBTYPE_CY = 6, + DBTYPE_DATE = 7, + DBTYPE_BSTR = 8, + DBTYPE_IDISPATCH = 9, + DBTYPE_ERROR = 10, + DBTYPE_BOOL = 11, + DBTYPE_VARIANT = 12, + DBTYPE_IUNKNOWN = 13, + DBTYPE_DECIMAL = 14, + DBTYPE_UI1 = 17, + DBTYPE_ARRAY = 0x2000, + DBTYPE_BYREF = 0x4000, + DBTYPE_I1 = 16, + DBTYPE_UI2 = 18, + DBTYPE_UI4 = 19, + DBTYPE_I8 = 20, + DBTYPE_UI8 = 21, + DBTYPE_GUID = 72, + DBTYPE_VECTOR = 0x1000, + DBTYPE_RESERVED = 0x8000, + DBTYPE_BYTES = 128, + DBTYPE_STR = 129, + DBTYPE_WSTR = 130, + DBTYPE_NUMERIC = 131, + DBTYPE_UDT = 132, + DBTYPE_DBDATE = 133, + DBTYPE_DBTIME = 134, + DBTYPE_DBTIMESTAMP = 135 + + ADO Types + + adEmpty = 0, + adTinyInt = 16, + adSmallInt = 2, + adInteger = 3, + adBigInt = 20, + adUnsignedTinyInt = 17, + adUnsignedSmallInt = 18, + adUnsignedInt = 19, + adUnsignedBigInt = 21, + adSingle = 4, + adDouble = 5, + adCurrency = 6, + adDecimal = 14, + adNumeric = 131, + adBoolean = 11, + adError = 10, + adUserDefined = 132, + adVariant = 12, + adIDispatch = 9, + adIUnknown = 13, + adGUID = 72, + adDate = 7, + adDBDate = 133, + adDBTime = 134, + adDBTimeStamp = 135, + adBSTR = 8, + adChar = 129, + adVarChar = 200, + adLongVarChar = 201, + adWChar = 130, + adVarWChar = 202, + adLongVarWChar = 203, + adBinary = 128, + adVarBinary = 204, + adLongVarBinary = 205, + adChapter = 136, + adFileTime = 64, + adDBFileTime = 137, + adPropVariant = 138, + adVarNumeric = 139 +*/ + function MetaType($t,$len=-1,$fieldobj=false) + { + if (is_object($t)) { + $fieldobj = $t; + $t = $fieldobj->type; + $len = $fieldobj->max_length; + } + + if (!is_numeric($t)) return $t; + + switch ($t) { + case 0: + case 12: // variant + case 8: // bstr + case 129: //char + case 130: //wc + case 200: // varc + case 202:// varWC + case 128: // bin + case 204: // varBin + case 72: // guid + if ($len <= $this->blobSize) return 'C'; + + case 201: + case 203: + return 'X'; + case 128: + case 204: + case 205: + return 'B'; + case 7: + case 133: return 'D'; + + case 134: + case 135: return 'T'; + + case 11: return 'L'; + + case 16:// adTinyInt = 16, + case 2://adSmallInt = 2, + case 3://adInteger = 3, + case 4://adBigInt = 20, + case 17://adUnsignedTinyInt = 17, + case 18://adUnsignedSmallInt = 18, + case 19://adUnsignedInt = 19, + case 20://adUnsignedBigInt = 21, + return 'I'; + default: return 'N'; + } + } + + // time stamp not supported yet + function _fetch() + { + $rs = $this->_queryID; + if (!$rs or $rs->EOF) { + $this->fields = false; + return false; + } + $this->fields = array(); + + if (!$this->_tarr) { + $tarr = array(); + $flds = array(); + for ($i=0,$max = $this->_numOfFields; $i < $max; $i++) { + $f = $rs->Fields($i); + $flds[] = $f; + $tarr[] = $f->Type; + } + // bind types and flds only once + $this->_tarr = $tarr; + $this->_flds = $flds; + } + $t = reset($this->_tarr); + $f = reset($this->_flds); + + if ($this->hideErrors) $olde = error_reporting(E_ERROR|E_CORE_ERROR);// sometimes $f->value be null + for ($i=0,$max = $this->_numOfFields; $i < $max; $i++) { + //echo "

",$t,' ';var_dump($f->value); echo '

'; + switch($t) { + case 135: // timestamp + if (!strlen((string)$f->value)) $this->fields[] = false; + else { + if (!is_numeric($f->value)) # $val = variant_date_to_timestamp($f->value); + // VT_DATE stores dates as (float) fractional days since 1899/12/30 00:00:00 + $val= (float) variant_cast($f->value,VT_R8)*3600*24-2209161600; + else + $val = $f->value; + $this->fields[] = adodb_date('Y-m-d H:i:s',$val); + } + break; + case 133:// A date value (yyyymmdd) + if ($val = $f->value) { + $this->fields[] = substr($val,0,4).'-'.substr($val,4,2).'-'.substr($val,6,2); + } else + $this->fields[] = false; + break; + case 7: // adDate + if (!strlen((string)$f->value)) $this->fields[] = false; + else { + if (!is_numeric($f->value)) $val = variant_date_to_timestamp($f->value); + else $val = $f->value; + + if (($val % 86400) == 0) $this->fields[] = adodb_date('Y-m-d',$val); + else $this->fields[] = adodb_date('Y-m-d H:i:s',$val); + } + break; + case 1: // null + $this->fields[] = false; + break; + case 20: + case 21: // bigint (64 bit) + $this->fields[] = (float) $f->value; // if 64 bit PHP, could use (int) + break; + case 6: // currency is not supported properly; + ADOConnection::outp( ''.$f->Name.': currency type not supported by PHP'); + $this->fields[] = (float) $f->value; + break; + case 11: //BIT; + $val = ""; + if(is_bool($f->value)) { + if($f->value==true) $val = 1; + else $val = 0; + } + if(is_null($f->value)) $val = null; + + $this->fields[] = $val; + break; + default: + $this->fields[] = $f->value; + break; + } + //print " $f->value $t, "; + $f = next($this->_flds); + $t = next($this->_tarr); + } // for + if ($this->hideErrors) error_reporting($olde); + @$rs->MoveNext(); // @ needed for some versions of PHP! + + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + $this->fields = $this->GetRowAssoc(); + } + return true; + } + + function NextRecordSet() + { + $rs = $this->_queryID; + $this->_queryID = $rs->NextRecordSet(); + //$this->_queryID = $this->_QueryId->NextRecordSet(); + if ($this->_queryID == null) return false; + + $this->_currentRow = -1; + $this->_currentPage = -1; + $this->bind = false; + $this->fields = false; + $this->_flds = false; + $this->_tarr = false; + + $this->_inited = false; + $this->Init(); + return true; + } + + function _close() { + $this->_flds = false; + try { + @$this->_queryID->Close();// by Pete Dishman (peterd@telephonetics.co.uk) + } catch (Exception $e) { + } + $this->_queryID = false; + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-ado_access.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-ado_access.inc.php new file mode 100644 index 000000000..c167ce63a --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-ado_access.inc.php @@ -0,0 +1,50 @@ += 5) include(ADODB_DIR."/drivers/adodb-ado5.inc.php"); + else include(ADODB_DIR."/drivers/adodb-ado.inc.php"); +} + +class ADODB_ado_access extends ADODB_ado { + var $databaseType = 'ado_access'; + var $hasTop = 'top'; // support mssql SELECT TOP 10 * FROM TABLE + var $fmtDate = "#Y-m-d#"; + var $fmtTimeStamp = "#Y-m-d h:i:sA#";// note no comma + var $sysDate = "FORMAT(NOW,'yyyy-mm-dd')"; + var $sysTimeStamp = 'NOW'; + var $upperCase = 'ucase'; + + /*function BeginTrans() { return false;} + + function CommitTrans() { return false;} + + function RollbackTrans() { return false;}*/ + +} + + +class ADORecordSet_ado_access extends ADORecordSet_ado { + + var $databaseType = "ado_access"; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-ado_mssql.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-ado_mssql.inc.php new file mode 100644 index 000000000..57eacc938 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-ado_mssql.inc.php @@ -0,0 +1,150 @@ += 5) include(ADODB_DIR."/drivers/adodb-ado5.inc.php"); + else include(ADODB_DIR."/drivers/adodb-ado.inc.php"); +} + + +class ADODB_ado_mssql extends ADODB_ado { + var $databaseType = 'ado_mssql'; + var $hasTop = 'top'; + var $hasInsertID = true; + var $sysDate = 'convert(datetime,convert(char,GetDate(),102),102)'; + var $sysTimeStamp = 'GetDate()'; + var $leftOuter = '*='; + var $rightOuter = '=*'; + var $ansiOuter = true; // for mssql7 or later + var $substr = "substring"; + var $length = 'len'; + var $_dropSeqSQL = "drop table %s"; + + //var $_inTransaction = 1; // always open recordsets, so no transaction problems. + + function _insertid() + { + return $this->GetOne('select SCOPE_IDENTITY()'); + } + + function _affectedrows() + { + return $this->GetOne('select @@rowcount'); + } + + function SetTransactionMode( $transaction_mode ) + { + $this->_transmode = $transaction_mode; + if (empty($transaction_mode)) { + $this->Execute('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); + return; + } + if (!stristr($transaction_mode,'isolation')) $transaction_mode = 'ISOLATION LEVEL '.$transaction_mode; + $this->Execute("SET TRANSACTION ".$transaction_mode); + } + + function qstr($s,$magic_quotes=false) + { + $s = ADOConnection::qstr($s, $magic_quotes); + return str_replace("\0", "\\\\000", $s); + } + + function MetaColumns($table, $normalize=true) + { + $table = strtoupper($table); + $arr= array(); + $dbc = $this->_connectionID; + + $osoptions = array(); + $osoptions[0] = null; + $osoptions[1] = null; + $osoptions[2] = $table; + $osoptions[3] = null; + + $adors=@$dbc->OpenSchema(4, $osoptions);//tables + + if ($adors){ + while (!$adors->EOF){ + $fld = new ADOFieldObject(); + $c = $adors->Fields(3); + $fld->name = $c->Value; + $fld->type = 'CHAR'; // cannot discover type in ADO! + $fld->max_length = -1; + $arr[strtoupper($fld->name)]=$fld; + + $adors->MoveNext(); + } + $adors->Close(); + } + $false = false; + return empty($arr) ? $false : $arr; + } + + function CreateSequence($seq='adodbseq',$start=1) + { + + $this->Execute('BEGIN TRANSACTION adodbseq'); + $start -= 1; + $this->Execute("create table $seq (id float(53))"); + $ok = $this->Execute("insert into $seq with (tablock,holdlock) values($start)"); + if (!$ok) { + $this->Execute('ROLLBACK TRANSACTION adodbseq'); + return false; + } + $this->Execute('COMMIT TRANSACTION adodbseq'); + return true; + } + + function GenID($seq='adodbseq',$start=1) + { + //$this->debug=1; + $this->Execute('BEGIN TRANSACTION adodbseq'); + $ok = $this->Execute("update $seq with (tablock,holdlock) set id = id + 1"); + if (!$ok) { + $this->Execute("create table $seq (id float(53))"); + $ok = $this->Execute("insert into $seq with (tablock,holdlock) values($start)"); + if (!$ok) { + $this->Execute('ROLLBACK TRANSACTION adodbseq'); + return false; + } + $this->Execute('COMMIT TRANSACTION adodbseq'); + return $start; + } + $num = $this->GetOne("select id from $seq"); + $this->Execute('COMMIT TRANSACTION adodbseq'); + return $num; + + // in old implementation, pre 1.90, we returned GUID... + //return $this->GetOne("SELECT CONVERT(varchar(255), NEWID()) AS 'Char'"); + } + + } // end class + + class ADORecordSet_ado_mssql extends ADORecordSet_ado { + + var $databaseType = 'ado_mssql'; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-ads.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-ads.inc.php new file mode 100644 index 000000000..d59df663a --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-ads.inc.php @@ -0,0 +1,784 @@ +_haserrorfunctions = ADODB_PHPVER >= 0x4050; + $this->_has_stupid_odbc_fetch_api_change = ADODB_PHPVER >= 0x4200; + } + + // returns true or false + function _connect($argDSN, $argUsername, $argPassword, $argDatabasename) + { + global $php_errormsg; + + if (!function_exists('ads_connect')) return null; + + if ($this->debug && $argDatabasename && $this->databaseType != 'vfp') { + ADOConnection::outp("For Advantage Connect(), $argDatabasename is not used. Place dsn in 1st parameter."); + } + if (isset($php_errormsg)) $php_errormsg = ''; + if ($this->curmode === false) $this->_connectionID = ads_connect($argDSN,$argUsername,$argPassword); + else $this->_connectionID = ads_connect($argDSN,$argUsername,$argPassword,$this->curmode); + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + if (isset($this->connectStmt)) $this->Execute($this->connectStmt); + + return $this->_connectionID != false; + } + + // returns true or false + function _pconnect($argDSN, $argUsername, $argPassword, $argDatabasename) + { + global $php_errormsg; + + if (!function_exists('ads_connect')) return null; + + if (isset($php_errormsg)) $php_errormsg = ''; + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + if ($this->debug && $argDatabasename) { + ADOConnection::outp("For PConnect(), $argDatabasename is not used. Place dsn in 1st parameter."); + } + // print "dsn=$argDSN u=$argUsername p=$argPassword
"; flush(); + if ($this->curmode === false) $this->_connectionID = ads_connect($argDSN,$argUsername,$argPassword); + else $this->_connectionID = ads_pconnect($argDSN,$argUsername,$argPassword,$this->curmode); + + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + if ($this->_connectionID && $this->autoRollback) @ads_rollback($this->_connectionID); + if (isset($this->connectStmt)) $this->Execute($this->connectStmt); + + return $this->_connectionID != false; + } + + // returns the Server version and Description + function ServerInfo() + { + + if (!empty($this->host) && ADODB_PHPVER >= 0x4300) { + $stmt = $this->Prepare('EXECUTE PROCEDURE sp_mgGetInstallInfo()'); + $res = $this->Execute($stmt); + if(!$res) + print $this->ErrorMsg(); + else{ + $ret["version"]= $res->fields[3]; + $ret["description"]="Advantage Database Server"; + return $ret; + } + } + else { + return ADOConnection::ServerInfo(); + } + } + + + // returns true or false + function CreateSequence($seqname = 'adodbseq', $start = 1) + { + $res = $this->Execute("CREATE TABLE $seqname ( ID autoinc( 1 ) ) IN DATABASE"); + if(!$res){ + print $this->ErrorMsg(); + return false; + } + else + return true; + + } + + // returns true or false + function DropSequence($seqname = 'adodbseq') + { + $res = $this->Execute("DROP TABLE $seqname"); + if(!$res){ + print $this->ErrorMsg(); + return false; + } + else + return true; + } + + + // returns the generated ID or false + // checks if the table already exists, else creates the table and inserts a record into the table + // and gets the ID number of the last inserted record. + function GenID($seqname = 'adodbseq', $start = 1) + { + $go = $this->Execute("select * from $seqname"); + if (!$go){ + $res = $this->Execute("CREATE TABLE $seqname ( ID autoinc( 1 ) ) IN DATABASE"); + if(!res){ + print $this->ErrorMsg(); + return false; + } + } + $res = $this->Execute("INSERT INTO $seqname VALUES( DEFAULT )"); + if(!$res){ + print $this->ErrorMsg(); + return false; + } + else{ + $gen = $this->Execute("SELECT LastAutoInc( STATEMENT ) FROM system.iota"); + $ret = $gen->fields[0]; + return $ret; + } + + } + + + + + function ErrorMsg() + { + if ($this->_haserrorfunctions) { + if ($this->_errorMsg !== false) return $this->_errorMsg; + if (empty($this->_connectionID)) return @ads_errormsg(); + return @ads_errormsg($this->_connectionID); + } else return ADOConnection::ErrorMsg(); + } + + + function ErrorNo() + { + + if ($this->_haserrorfunctions) { + if ($this->_errorCode !== false) { + // bug in 4.0.6, error number can be corrupted string (should be 6 digits) + return (strlen($this->_errorCode)<=2) ? 0 : $this->_errorCode; + } + + if (empty($this->_connectionID)) $e = @ads_error(); + else $e = @ads_error($this->_connectionID); + + // bug in 4.0.6, error number can be corrupted string (should be 6 digits) + // so we check and patch + if (strlen($e)<=2) return 0; + return $e; + } else return ADOConnection::ErrorNo(); + } + + + + function BeginTrans() + { + if (!$this->hasTransactions) return false; + if ($this->transOff) return true; + $this->transCnt += 1; + $this->_autocommit = false; + return ads_autocommit($this->_connectionID,false); + } + + function CommitTrans($ok=true) + { + if ($this->transOff) return true; + if (!$ok) return $this->RollbackTrans(); + if ($this->transCnt) $this->transCnt -= 1; + $this->_autocommit = true; + $ret = ads_commit($this->_connectionID); + ads_autocommit($this->_connectionID,true); + return $ret; + } + + function RollbackTrans() + { + if ($this->transOff) return true; + if ($this->transCnt) $this->transCnt -= 1; + $this->_autocommit = true; + $ret = ads_rollback($this->_connectionID); + ads_autocommit($this->_connectionID,true); + return $ret; + } + + + // Returns tables,Views or both on succesfull execution. Returns + // tables by default on succesfull execustion. + function &MetaTables($ttype = false, $showSchema = false, $mask = false) + { + $recordSet1 = $this->Execute("select * from system.tables"); + if(!$recordSet1){ + print $this->ErrorMsg(); + return false; + } + $recordSet2 = $this->Execute("select * from system.views"); + if(!$recordSet2){ + print $this->ErrorMsg(); + return false; + } + $i=0; + while (!$recordSet1->EOF){ + $arr["$i"] = $recordSet1->fields[0]; + $recordSet1->MoveNext(); + $i=$i+1; + } + if($ttype=='FALSE'){ + while (!$recordSet2->EOF){ + $arr["$i"] = $recordSet2->fields[0]; + $recordSet2->MoveNext(); + $i=$i+1; + } + return $arr; + } + elseif($ttype=='VIEWS'){ + while (!$recordSet2->EOF){ + $arrV["$i"] = $recordSet2->fields[0]; + $recordSet2->MoveNext(); + $i=$i+1; + } + return $arrV; + } + else{ + return $arr; + } + + } + + function &MetaPrimaryKeys($table, $owner = false) + { + $recordSet = $this->Execute("select table_primary_key from system.tables where name='$table'"); + if(!$recordSet){ + print $this->ErrorMsg(); + return false; + } + $i=0; + while (!$recordSet->EOF){ + $arr["$i"] = $recordSet->fields[0]; + $recordSet->MoveNext(); + $i=$i+1; + } + return $arr; + } + +/* +See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/odbc/htm/odbcdatetime_data_type_changes.asp +/ SQL data type codes / +#define SQL_UNKNOWN_TYPE 0 +#define SQL_CHAR 1 +#define SQL_NUMERIC 2 +#define SQL_DECIMAL 3 +#define SQL_INTEGER 4 +#define SQL_SMALLINT 5 +#define SQL_FLOAT 6 +#define SQL_REAL 7 +#define SQL_DOUBLE 8 +#if (ODBCVER >= 0x0300) +#define SQL_DATETIME 9 +#endif +#define SQL_VARCHAR 12 + + +/ One-parameter shortcuts for date/time data types / +#if (ODBCVER >= 0x0300) +#define SQL_TYPE_DATE 91 +#define SQL_TYPE_TIME 92 +#define SQL_TYPE_TIMESTAMP 93 + +#define SQL_UNICODE (-95) +#define SQL_UNICODE_VARCHAR (-96) +#define SQL_UNICODE_LONGVARCHAR (-97) +*/ + function ODBCTypes($t) + { + switch ((integer)$t) { + case 1: + case 12: + case 0: + case -95: + case -96: + return 'C'; + case -97: + case -1: //text + return 'X'; + case -4: //image + return 'B'; + + case 9: + case 91: + return 'D'; + + case 10: + case 11: + case 92: + case 93: + return 'T'; + + case 4: + case 5: + case -6: + return 'I'; + + case -11: // uniqidentifier + return 'R'; + case -7: //bit + return 'L'; + + default: + return 'N'; + } + } + + function &MetaColumns($table, $normalize = true) + { + global $ADODB_FETCH_MODE; + + $false = false; + if ($this->uCaseTables) $table = strtoupper($table); + $schema = ''; + $this->_findschema($table,$schema); + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + /*if (false) { // after testing, confirmed that the following does not work becoz of a bug + $qid2 = ads_tables($this->_connectionID); + $rs = new ADORecordSet_ads($qid2); + $ADODB_FETCH_MODE = $savem; + if (!$rs) return false; + $rs->_has_stupid_odbc_fetch_api_change = $this->_has_stupid_odbc_fetch_api_change; + $rs->_fetch(); + + while (!$rs->EOF) { + if ($table == strtoupper($rs->fields[2])) { + $q = $rs->fields[0]; + $o = $rs->fields[1]; + break; + } + $rs->MoveNext(); + } + $rs->Close(); + + $qid = ads_columns($this->_connectionID,$q,$o,strtoupper($table),'%'); + } */ + + switch ($this->databaseType) { + case 'access': + case 'vfp': + $qid = ads_columns($this->_connectionID);#,'%','',strtoupper($table),'%'); + break; + + + case 'db2': + $colname = "%"; + $qid = ads_columns($this->_connectionID, "", $schema, $table, $colname); + break; + + default: + $qid = @ads_columns($this->_connectionID,'%','%',strtoupper($table),'%'); + if (empty($qid)) $qid = ads_columns($this->_connectionID); + break; + } + if (empty($qid)) return $false; + + $rs = new ADORecordSet_ads($qid); + $ADODB_FETCH_MODE = $savem; + + if (!$rs) return $false; + $rs->_has_stupid_odbc_fetch_api_change = $this->_has_stupid_odbc_fetch_api_change; + $rs->_fetch(); + + $retarr = array(); + + /* + $rs->fields indices + 0 TABLE_QUALIFIER + 1 TABLE_SCHEM + 2 TABLE_NAME + 3 COLUMN_NAME + 4 DATA_TYPE + 5 TYPE_NAME + 6 PRECISION + 7 LENGTH + 8 SCALE + 9 RADIX + 10 NULLABLE + 11 REMARKS + */ + while (!$rs->EOF) { + // adodb_pr($rs->fields); + if (strtoupper(trim($rs->fields[2])) == $table && (!$schema || strtoupper($rs->fields[1]) == $schema)) { + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[3]; + $fld->type = $this->ODBCTypes($rs->fields[4]); + + // ref: http://msdn.microsoft.com/library/default.asp?url=/archive/en-us/dnaraccgen/html/msdn_odk.asp + // access uses precision to store length for char/varchar + if ($fld->type == 'C' or $fld->type == 'X') { + if ($this->databaseType == 'access') + $fld->max_length = $rs->fields[6]; + else if ($rs->fields[4] <= -95) // UNICODE + $fld->max_length = $rs->fields[7]/2; + else + $fld->max_length = $rs->fields[7]; + } else + $fld->max_length = $rs->fields[7]; + $fld->not_null = !empty($rs->fields[10]); + $fld->scale = $rs->fields[8]; + $retarr[strtoupper($fld->name)] = $fld; + } else if (sizeof($retarr)>0) + break; + $rs->MoveNext(); + } + $rs->Close(); //-- crashes 4.03pl1 -- why? + + if (empty($retarr)) $retarr = false; + return $retarr; + } + + // Returns an array of columns names for a given table + function &MetaColumnNames($table, $numIndexes = false, $useattnum = false) + { + $recordSet = $this->Execute("select name from system.columns where parent='$table'"); + if(!$recordSet){ + print $this->ErrorMsg(); + return false; + } + else{ + $i=0; + while (!$recordSet->EOF){ + $arr["FIELD$i"] = $recordSet->fields[0]; + $recordSet->MoveNext(); + $i=$i+1; + } + return $arr; + } + } + + + function Prepare($sql) + { + if (! $this->_bindInputArray) return $sql; // no binding + $stmt = ads_prepare($this->_connectionID,$sql); + if (!$stmt) { + // we don't know whether odbc driver is parsing prepared stmts, so just return sql + return $sql; + } + return array($sql,$stmt,false); + } + + /* returns queryID or false */ + function _query($sql,$inputarr=false) + { + GLOBAL $php_errormsg; + if (isset($php_errormsg)) $php_errormsg = ''; + $this->_error = ''; + + if ($inputarr) { + if (is_array($sql)) { + $stmtid = $sql[1]; + } else { + $stmtid = ads_prepare($this->_connectionID,$sql); + + if ($stmtid == false) { + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + return false; + } + } + + if (! ads_execute($stmtid,$inputarr)) { + //@ads_free_result($stmtid); + if ($this->_haserrorfunctions) { + $this->_errorMsg = ads_errormsg(); + $this->_errorCode = ads_error(); + } + return false; + } + + } else if (is_array($sql)) { + $stmtid = $sql[1]; + if (!ads_execute($stmtid)) { + //@ads_free_result($stmtid); + if ($this->_haserrorfunctions) { + $this->_errorMsg = ads_errormsg(); + $this->_errorCode = ads_error(); + } + return false; + } + } else + { + + $stmtid = ads_exec($this->_connectionID,$sql); + + } + + $this->_lastAffectedRows = 0; + + if ($stmtid) + { + + if (@ads_num_fields($stmtid) == 0) { + $this->_lastAffectedRows = ads_num_rows($stmtid); + $stmtid = true; + + } else { + + $this->_lastAffectedRows = 0; + ads_binmode($stmtid,$this->binmode); + ads_longreadlen($stmtid,$this->maxblobsize); + + } + + if ($this->_haserrorfunctions) + { + + $this->_errorMsg = ''; + $this->_errorCode = 0; + } + else + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + } + else + { + if ($this->_haserrorfunctions) { + $this->_errorMsg = ads_errormsg(); + $this->_errorCode = ads_error(); + } else + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + } + + return $stmtid; + + } + + /* + Insert a null into the blob field of the table first. + Then use UpdateBlob to store the blob. + + Usage: + + $conn->Execute('INSERT INTO blobtable (id, blobcol) VALUES (1, null)'); + $conn->UpdateBlob('blobtable','blobcol',$blob,'id=1'); + */ + function UpdateBlob($table,$column,$val,$where,$blobtype='BLOB') + { + $sql = "UPDATE $table SET $column=? WHERE $where"; + $stmtid = ads_prepare($this->_connectionID,$sql); + if ($stmtid == false){ + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + return false; + } + if (! ads_execute($stmtid,array($val),array(SQL_BINARY) )){ + if ($this->_haserrorfunctions){ + $this->_errorMsg = ads_errormsg(); + $this->_errorCode = ads_error(); + } + return false; + } + return TRUE; + } + + // returns true or false + function _close() + { + $ret = @ads_close($this->_connectionID); + $this->_connectionID = false; + return $ret; + } + + function _affectedrows() + { + return $this->_lastAffectedRows; + } + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordSet_ads extends ADORecordSet { + + var $bind = false; + var $databaseType = "ads"; + var $dataProvider = "ads"; + var $useFetchArray; + var $_has_stupid_odbc_fetch_api_change; + + function __construct($id,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + $this->fetchMode = $mode; + + $this->_queryID = $id; + + // the following is required for mysql odbc driver in 4.3.1 -- why? + $this->EOF = false; + $this->_currentRow = -1; + //parent::__construct($id); + } + + + // returns the field object + function &FetchField($fieldOffset = -1) + { + + $off=$fieldOffset+1; // offsets begin at 1 + + $o= new ADOFieldObject(); + $o->name = @ads_field_name($this->_queryID,$off); + $o->type = @ads_field_type($this->_queryID,$off); + $o->max_length = @ads_field_len($this->_queryID,$off); + if (ADODB_ASSOC_CASE == 0) $o->name = strtolower($o->name); + else if (ADODB_ASSOC_CASE == 1) $o->name = strtoupper($o->name); + return $o; + } + + /* Use associative array to get fields array */ + function Fields($colname) + { + if ($this->fetchMode & ADODB_FETCH_ASSOC) return $this->fields[$colname]; + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + + function _initrs() + { + global $ADODB_COUNTRECS; + $this->_numOfRows = ($ADODB_COUNTRECS) ? @ads_num_rows($this->_queryID) : -1; + $this->_numOfFields = @ads_num_fields($this->_queryID); + // some silly drivers such as db2 as/400 and intersystems cache return _numOfRows = 0 + if ($this->_numOfRows == 0) $this->_numOfRows = -1; + //$this->useFetchArray = $this->connection->useFetchArray; + $this->_has_stupid_odbc_fetch_api_change = ADODB_PHPVER >= 0x4200; + } + + function _seek($row) + { + return false; + } + + // speed up SelectLimit() by switching to ADODB_FETCH_NUM as ADODB_FETCH_ASSOC is emulated + function &GetArrayLimit($nrows,$offset=-1) + { + if ($offset <= 0) { + $rs =& $this->GetArray($nrows); + return $rs; + } + $savem = $this->fetchMode; + $this->fetchMode = ADODB_FETCH_NUM; + $this->Move($offset); + $this->fetchMode = $savem; + + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + $this->fields =& $this->GetRowAssoc(); + } + + $results = array(); + $cnt = 0; + while (!$this->EOF && $nrows != $cnt) { + $results[$cnt++] = $this->fields; + $this->MoveNext(); + } + + return $results; + } + + + function MoveNext() + { + if ($this->_numOfRows != 0 && !$this->EOF) { + $this->_currentRow++; + if( $this->_fetch() ) { + return true; + } + } + $this->fields = false; + $this->EOF = true; + return false; + } + + function _fetch() + { + $this->fields = false; + if ($this->_has_stupid_odbc_fetch_api_change) + $rez = @ads_fetch_into($this->_queryID,$this->fields); + else { + $row = 0; + $rez = @ads_fetch_into($this->_queryID,$row,$this->fields); + } + if ($rez) { + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + $this->fields =& $this->GetRowAssoc(); + } + return true; + } + return false; + } + + function _close() + { + return @ads_free_result($this->_queryID); + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-borland_ibase.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-borland_ibase.inc.php new file mode 100644 index 000000000..d3de2caa1 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-borland_ibase.inc.php @@ -0,0 +1,87 @@ +transOff) return true; + $this->transCnt += 1; + $this->autoCommit = false; + $this->_transactionID = ibase_trans($this->ibasetrans, $this->_connectionID); + return $this->_transactionID; + } + + function ServerInfo() + { + $arr['dialect'] = $this->dialect; + switch($arr['dialect']) { + case '': + case '1': $s = 'Interbase 6.5, Dialect 1'; break; + case '2': $s = 'Interbase 6.5, Dialect 2'; break; + default: + case '3': $s = 'Interbase 6.5, Dialect 3'; break; + } + $arr['version'] = '6.5'; + $arr['description'] = $s; + return $arr; + } + + // Note that Interbase 6.5 uses ROWS instead - don't you love forking wars! + // SELECT col1, col2 FROM table ROWS 5 -- get 5 rows + // SELECT col1, col2 FROM TABLE ORDER BY col1 ROWS 3 TO 7 -- first 5 skip 2 + // Firebird uses + // SELECT FIRST 5 SKIP 2 col1, col2 FROM TABLE + function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false,$secs2cache=0) + { + if ($nrows > 0) { + if ($offset <= 0) $str = " ROWS $nrows "; + else { + $a = $offset+1; + $b = $offset+$nrows; + $str = " ROWS $a TO $b"; + } + } else { + // ok, skip + $a = $offset + 1; + $str = " ROWS $a TO 999999999"; // 999 million + } + $sql .= $str; + + return ($secs2cache) ? + $this->CacheExecute($secs2cache,$sql,$inputarr) + : + $this->Execute($sql,$inputarr); + } + +}; + + +class ADORecordSet_borland_ibase extends ADORecordSet_ibase { + + var $databaseType = "borland_ibase"; + + function __construct($id,$mode=false) + { + parent::__construct($id,$mode); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-csv.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-csv.inc.php new file mode 100644 index 000000000..fd47784de --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-csv.inc.php @@ -0,0 +1,207 @@ +_insertid; + } + + function _affectedrows() + { + return $this->_affectedrows; + } + + function MetaDatabases() + { + return false; + } + + + // returns true or false + function _connect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + if (strtolower(substr($argHostname,0,7)) !== 'http://') return false; + $this->_url = $argHostname; + return true; + } + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + if (strtolower(substr($argHostname,0,7)) !== 'http://') return false; + $this->_url = $argHostname; + return true; + } + + function MetaColumns($table, $normalize=true) + { + return false; + } + + + // parameters use PostgreSQL convention, not MySQL + function SelectLimit($sql, $nrows = -1, $offset = -1, $inputarr = false, $secs2cache = 0) + { + global $ADODB_FETCH_MODE; + + $url = $this->_url.'?sql='.urlencode($sql)."&nrows=$nrows&fetch=". + (($this->fetchMode !== false)?$this->fetchMode : $ADODB_FETCH_MODE). + "&offset=$offset"; + $err = false; + $rs = csv2rs($url,$err,false); + + if ($this->debug) print "$url
$err
"; + + $at = strpos($err,'::::'); + if ($at === false) { + $this->_errorMsg = $err; + $this->_errorNo = (integer)$err; + } else { + $this->_errorMsg = substr($err,$at+4,1024); + $this->_errorNo = -9999; + } + if ($this->_errorNo) + if ($fn = $this->raiseErrorFn) { + $fn($this->databaseType,'EXECUTE',$this->ErrorNo(),$this->ErrorMsg(),$sql,''); + } + + if (is_object($rs)) { + + $rs->databaseType='csv'; + $rs->fetchMode = ($this->fetchMode !== false) ? $this->fetchMode : $ADODB_FETCH_MODE; + $rs->connection = $this; + } + return $rs; + } + + // returns queryID or false + function _Execute($sql,$inputarr=false) + { + global $ADODB_FETCH_MODE; + + if (!$this->_bindInputArray && $inputarr) { + $sqlarr = explode('?',$sql); + $sql = ''; + $i = 0; + foreach($inputarr as $v) { + + $sql .= $sqlarr[$i]; + if (gettype($v) == 'string') + $sql .= $this->qstr($v); + else if ($v === null) + $sql .= 'NULL'; + else + $sql .= $v; + $i += 1; + + } + $sql .= $sqlarr[$i]; + if ($i+1 != sizeof($sqlarr)) + print "Input Array does not match ?: ".htmlspecialchars($sql); + $inputarr = false; + } + + $url = $this->_url.'?sql='.urlencode($sql)."&fetch=". + (($this->fetchMode !== false)?$this->fetchMode : $ADODB_FETCH_MODE); + $err = false; + + + $rs = csv2rs($url,$err,false); + if ($this->debug) print urldecode($url)."
$err
"; + $at = strpos($err,'::::'); + if ($at === false) { + $this->_errorMsg = $err; + $this->_errorNo = (integer)$err; + } else { + $this->_errorMsg = substr($err,$at+4,1024); + $this->_errorNo = -9999; + } + + if ($this->_errorNo) + if ($fn = $this->raiseErrorFn) { + $fn($this->databaseType,'EXECUTE',$this->ErrorNo(),$this->ErrorMsg(),$sql,$inputarr); + } + if (is_object($rs)) { + $rs->fetchMode = ($this->fetchMode !== false) ? $this->fetchMode : $ADODB_FETCH_MODE; + + $this->_affectedrows = $rs->affectedrows; + $this->_insertid = $rs->insertid; + $rs->databaseType='csv'; + $rs->connection = $this; + } + return $rs; + } + + /* Returns: the last error message from previous database operation */ + function ErrorMsg() + { + return $this->_errorMsg; + } + + /* Returns: the last error number from previous database operation */ + function ErrorNo() + { + return $this->_errorNo; + } + + // returns true or false + function _close() + { + return true; + } +} // class + +class ADORecordset_csv extends ADORecordset { + function __construct($id,$mode=false) + { + parent::__construct($id,$mode); + } + + function _close() + { + return true; + } +} + +} // define diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-db2.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-db2.inc.php new file mode 100644 index 000000000..e7b9dbdd7 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-db2.inc.php @@ -0,0 +1,849 @@ +_haserrorfunctions = ADODB_PHPVER >= 0x4050; + } + + // returns true or false + function _connect($argDSN, $argUsername, $argPassword, $argDatabasename) + { + global $php_errormsg; + + if (!function_exists('db2_connect')) { + ADOConnection::outp("Warning: The old ODBC based DB2 driver has been renamed 'odbc_db2'. This ADOdb driver calls PHP's native db2 extension which is not installed."); + return null; + } + // This needs to be set before the connect(). + // Replaces the odbc_binmode() call that was in Execute() + ini_set('ibm_db2.binmode', $this->binmode); + + if ($argDatabasename && empty($argDSN)) { + + if (stripos($argDatabasename,'UID=') && stripos($argDatabasename,'PWD=')) $this->_connectionID = db2_connect($argDatabasename,null,null); + else $this->_connectionID = db2_connect($argDatabasename,$argUsername,$argPassword); + } else { + if ($argDatabasename) $schema = $argDatabasename; + if (stripos($argDSN,'UID=') && stripos($argDSN,'PWD=')) $this->_connectionID = db2_connect($argDSN,null,null); + else $this->_connectionID = db2_connect($argDSN,$argUsername,$argPassword); + } + if (isset($php_errormsg)) $php_errormsg = ''; + + // For db2_connect(), there is an optional 4th arg. If present, it must be + // an array of valid options. So far, we don't use them. + + $this->_errorMsg = @db2_conn_errormsg(); + if (isset($this->connectStmt)) $this->Execute($this->connectStmt); + + if ($this->_connectionID && isset($schema)) $this->Execute("SET SCHEMA=$schema"); + return $this->_connectionID != false; + } + + // returns true or false + function _pconnect($argDSN, $argUsername, $argPassword, $argDatabasename) + { + global $php_errormsg; + + if (!function_exists('db2_connect')) return null; + + // This needs to be set before the connect(). + // Replaces the odbc_binmode() call that was in Execute() + ini_set('ibm_db2.binmode', $this->binmode); + + if (isset($php_errormsg)) $php_errormsg = ''; + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + + if ($argDatabasename && empty($argDSN)) { + + if (stripos($argDatabasename,'UID=') && stripos($argDatabasename,'PWD=')) $this->_connectionID = db2_pconnect($argDatabasename,null,null); + else $this->_connectionID = db2_pconnect($argDatabasename,$argUsername,$argPassword); + } else { + if ($argDatabasename) $schema = $argDatabasename; + if (stripos($argDSN,'UID=') && stripos($argDSN,'PWD=')) $this->_connectionID = db2_pconnect($argDSN,null,null); + else $this->_connectionID = db2_pconnect($argDSN,$argUsername,$argPassword); + } + if (isset($php_errormsg)) $php_errormsg = ''; + + $this->_errorMsg = @db2_conn_errormsg(); + if ($this->_connectionID && $this->autoRollback) @db2_rollback($this->_connectionID); + if (isset($this->connectStmt)) $this->Execute($this->connectStmt); + + if ($this->_connectionID && isset($schema)) $this->Execute("SET SCHEMA=$schema"); + return $this->_connectionID != false; + } + + // format and return date string in database timestamp format + function DBTimeStamp($ts, $isfld = false) + { + if (empty($ts) && $ts !== 0) return 'null'; + if (is_string($ts)) $ts = ADORecordSet::UnixTimeStamp($ts); + return 'TO_DATE('.adodb_date($this->fmtTimeStamp,$ts).",'YYYY-MM-DD HH24:MI:SS')"; + } + + // Format date column in sql string given an input format that understands Y M D + function SQLDate($fmt, $col=false) + { + // use right() and replace() ? + if (!$col) $col = $this->sysDate; + + /* use TO_CHAR() if $fmt is TO_CHAR() allowed fmt */ + if ($fmt== 'Y-m-d H:i:s') + return 'TO_CHAR('.$col.", 'YYYY-MM-DD HH24:MI:SS')"; + + $s = ''; + + $len = strlen($fmt); + for ($i=0; $i < $len; $i++) { + if ($s) $s .= $this->concat_operator; + $ch = $fmt[$i]; + switch($ch) { + case 'Y': + case 'y': + if ($len==1) return "year($col)"; + $s .= "char(year($col))"; + break; + case 'M': + if ($len==1) return "monthname($col)"; + $s .= "substr(monthname($col),1,3)"; + break; + case 'm': + if ($len==1) return "month($col)"; + $s .= "right(digits(month($col)),2)"; + break; + case 'D': + case 'd': + if ($len==1) return "day($col)"; + $s .= "right(digits(day($col)),2)"; + break; + case 'H': + case 'h': + if ($len==1) return "hour($col)"; + if ($col != $this->sysDate) $s .= "right(digits(hour($col)),2)"; + else $s .= "''"; + break; + case 'i': + case 'I': + if ($len==1) return "minute($col)"; + if ($col != $this->sysDate) + $s .= "right(digits(minute($col)),2)"; + else $s .= "''"; + break; + case 'S': + case 's': + if ($len==1) return "second($col)"; + if ($col != $this->sysDate) + $s .= "right(digits(second($col)),2)"; + else $s .= "''"; + break; + default: + if ($ch == '\\') { + $i++; + $ch = substr($fmt,$i,1); + } + $s .= $this->qstr($ch); + } + } + return $s; + } + + + function ServerInfo() + { + $row = $this->GetRow("SELECT service_level, fixpack_num FROM TABLE(sysproc.env_get_inst_info()) + as INSTANCEINFO"); + + + if ($row) { + $info['version'] = $row[0].':'.$row[1]; + $info['fixpack'] = $row[1]; + $info['description'] = ''; + } else { + return ADOConnection::ServerInfo(); + } + + return $info; + } + + function CreateSequence($seqname='adodbseq',$start=1) + { + if (empty($this->_genSeqSQL)) return false; + $ok = $this->Execute(sprintf($this->_genSeqSQL,$seqname,$start)); + if (!$ok) return false; + return true; + } + + function DropSequence($seqname = 'adodbseq') + { + if (empty($this->_dropSeqSQL)) return false; + return $this->Execute(sprintf($this->_dropSeqSQL,$seqname)); + } + + function SelectLimit($sql, $nrows = -1, $offset = -1, $inputArr = false, $secs2cache = 0) + { + $nrows = (integer) $nrows; + if ($offset <= 0) { + // could also use " OPTIMIZE FOR $nrows ROWS " + if ($nrows >= 0) $sql .= " FETCH FIRST $nrows ROWS ONLY "; + $rs = $this->Execute($sql,$inputArr); + } else { + if ($offset > 0 && $nrows < 0); + else { + $nrows += $offset; + $sql .= " FETCH FIRST $nrows ROWS ONLY "; + } + $rs = ADOConnection::SelectLimit($sql,-1,$offset,$inputArr); + } + + return $rs; + } + + /* + This algorithm is not very efficient, but works even if table locking + is not available. + + Will return false if unable to generate an ID after $MAXLOOPS attempts. + */ + function GenID($seq='adodbseq',$start=1) + { + // if you have to modify the parameter below, your database is overloaded, + // or you need to implement generation of id's yourself! + $num = $this->GetOne("VALUES NEXTVAL FOR $seq"); + return $num; + } + + + function ErrorMsg() + { + if ($this->_haserrorfunctions) { + if ($this->_errorMsg !== false) return $this->_errorMsg; + if (empty($this->_connectionID)) return @db2_conn_errormsg(); + return @db2_conn_errormsg($this->_connectionID); + } else return ADOConnection::ErrorMsg(); + } + + function ErrorNo() + { + + if ($this->_haserrorfunctions) { + if ($this->_errorCode !== false) { + // bug in 4.0.6, error number can be corrupted string (should be 6 digits) + return (strlen($this->_errorCode)<=2) ? 0 : $this->_errorCode; + } + + if (empty($this->_connectionID)) $e = @db2_conn_error(); + else $e = @db2_conn_error($this->_connectionID); + + // bug in 4.0.6, error number can be corrupted string (should be 6 digits) + // so we check and patch + if (strlen($e)<=2) return 0; + return $e; + } else return ADOConnection::ErrorNo(); + } + + + + function BeginTrans() + { + if (!$this->hasTransactions) return false; + if ($this->transOff) return true; + $this->transCnt += 1; + $this->_autocommit = false; + return db2_autocommit($this->_connectionID,false); + } + + function CommitTrans($ok=true) + { + if ($this->transOff) return true; + if (!$ok) return $this->RollbackTrans(); + if ($this->transCnt) $this->transCnt -= 1; + $this->_autocommit = true; + $ret = db2_commit($this->_connectionID); + db2_autocommit($this->_connectionID,true); + return $ret; + } + + function RollbackTrans() + { + if ($this->transOff) return true; + if ($this->transCnt) $this->transCnt -= 1; + $this->_autocommit = true; + $ret = db2_rollback($this->_connectionID); + db2_autocommit($this->_connectionID,true); + return $ret; + } + + function MetaPrimaryKeys($table, $owner = false) + { + global $ADODB_FETCH_MODE; + + if ($this->uCaseTables) $table = strtoupper($table); + $schema = ''; + $this->_findschema($table,$schema); + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $qid = @db2_primarykeys($this->_connectionID,'',$schema,$table); + + if (!$qid) { + $ADODB_FETCH_MODE = $savem; + return false; + } + $rs = new ADORecordSet_db2($qid); + $ADODB_FETCH_MODE = $savem; + + if (!$rs) return false; + + $arr = $rs->GetArray(); + $rs->Close(); + $arr2 = array(); + for ($i=0; $i < sizeof($arr); $i++) { + if ($arr[$i][3]) $arr2[] = $arr[$i][3]; + } + return $arr2; + } + + function MetaForeignKeys($table, $owner = FALSE, $upper = FALSE, $asociative = FALSE ) + { + global $ADODB_FETCH_MODE; + + if ($this->uCaseTables) $table = strtoupper($table); + $schema = ''; + $this->_findschema($table,$schema); + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $qid = @db2_foreign_keys($this->_connectionID,'',$schema,$table); + if (!$qid) { + $ADODB_FETCH_MODE = $savem; + return false; + } + $rs = new ADORecordSet_db2($qid); + + $ADODB_FETCH_MODE = $savem; + /* + $rs->fields indices + 0 PKTABLE_CAT + 1 PKTABLE_SCHEM + 2 PKTABLE_NAME + 3 PKCOLUMN_NAME + 4 FKTABLE_CAT + 5 FKTABLE_SCHEM + 6 FKTABLE_NAME + 7 FKCOLUMN_NAME + */ + if (!$rs) return false; + + $foreign_keys = array(); + while (!$rs->EOF) { + if (strtoupper(trim($rs->fields[2])) == $table && (!$schema || strtoupper($rs->fields[1]) == $schema)) { + if (!is_array($foreign_keys[$rs->fields[5].'.'.$rs->fields[6]])) + $foreign_keys[$rs->fields[5].'.'.$rs->fields[6]] = array(); + $foreign_keys[$rs->fields[5].'.'.$rs->fields[6]][$rs->fields[7]] = $rs->fields[3]; + } + $rs->MoveNext(); + } + + $rs->Close(); + return $foreign_key; + } + + + function MetaTables($ttype = false, $schema = false, $mask = false) + { + global $ADODB_FETCH_MODE; + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $qid = db2_tables($this->_connectionID); + + $rs = new ADORecordSet_db2($qid); + + $ADODB_FETCH_MODE = $savem; + if (!$rs) { + $false = false; + return $false; + } + + $arr = $rs->GetArray(); + $rs->Close(); + $arr2 = array(); + + if ($ttype) { + $isview = strncmp($ttype,'V',1) === 0; + } + for ($i=0; $i < sizeof($arr); $i++) { + if (!$arr[$i][2]) continue; + $type = $arr[$i][3]; + $owner = $arr[$i][1]; + $schemaval = ($schema) ? $arr[$i][1].'.' : ''; + if ($ttype) { + if ($isview) { + if (strncmp($type,'V',1) === 0) $arr2[] = $schemaval.$arr[$i][2]; + } else if (strncmp($owner,'SYS',3) !== 0) $arr2[] = $schemaval.$arr[$i][2]; + } else if (strncmp($owner,'SYS',3) !== 0) $arr2[] = $schemaval.$arr[$i][2]; + } + return $arr2; + } + +/* +See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/db2/htm/db2datetime_data_type_changes.asp +/ SQL data type codes / +#define SQL_UNKNOWN_TYPE 0 +#define SQL_CHAR 1 +#define SQL_NUMERIC 2 +#define SQL_DECIMAL 3 +#define SQL_INTEGER 4 +#define SQL_SMALLINT 5 +#define SQL_FLOAT 6 +#define SQL_REAL 7 +#define SQL_DOUBLE 8 +#if (DB2VER >= 0x0300) +#define SQL_DATETIME 9 +#endif +#define SQL_VARCHAR 12 + + +/ One-parameter shortcuts for date/time data types / +#if (DB2VER >= 0x0300) +#define SQL_TYPE_DATE 91 +#define SQL_TYPE_TIME 92 +#define SQL_TYPE_TIMESTAMP 93 + +#define SQL_UNICODE (-95) +#define SQL_UNICODE_VARCHAR (-96) +#define SQL_UNICODE_LONGVARCHAR (-97) +*/ + function DB2Types($t) + { + switch ((integer)$t) { + case 1: + case 12: + case 0: + case -95: + case -96: + return 'C'; + case -97: + case -1: //text + return 'X'; + case -4: //image + return 'B'; + + case 9: + case 91: + return 'D'; + + case 10: + case 11: + case 92: + case 93: + return 'T'; + + case 4: + case 5: + case -6: + return 'I'; + + case -11: // uniqidentifier + return 'R'; + case -7: //bit + return 'L'; + + default: + return 'N'; + } + } + + function MetaColumns($table, $normalize=true) + { + global $ADODB_FETCH_MODE; + + $false = false; + if ($this->uCaseTables) $table = strtoupper($table); + $schema = ''; + $this->_findschema($table,$schema); + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + $colname = "%"; + $qid = db2_columns($this->_connectionID, "", $schema, $table, $colname); + if (empty($qid)) return $false; + + $rs = new ADORecordSet_db2($qid); + $ADODB_FETCH_MODE = $savem; + + if (!$rs) return $false; + $rs->_fetch(); + + $retarr = array(); + + /* + $rs->fields indices + 0 TABLE_QUALIFIER + 1 TABLE_SCHEM + 2 TABLE_NAME + 3 COLUMN_NAME + 4 DATA_TYPE + 5 TYPE_NAME + 6 PRECISION + 7 LENGTH + 8 SCALE + 9 RADIX + 10 NULLABLE + 11 REMARKS + */ + while (!$rs->EOF) { + if (strtoupper(trim($rs->fields[2])) == $table && (!$schema || strtoupper($rs->fields[1]) == $schema)) { + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[3]; + $fld->type = $this->DB2Types($rs->fields[4]); + + // ref: http://msdn.microsoft.com/library/default.asp?url=/archive/en-us/dnaraccgen/html/msdn_odk.asp + // access uses precision to store length for char/varchar + if ($fld->type == 'C' or $fld->type == 'X') { + if ($rs->fields[4] <= -95) // UNICODE + $fld->max_length = $rs->fields[7]/2; + else + $fld->max_length = $rs->fields[7]; + } else + $fld->max_length = $rs->fields[7]; + $fld->not_null = !empty($rs->fields[10]); + $fld->scale = $rs->fields[8]; + $fld->primary_key = false; + $retarr[strtoupper($fld->name)] = $fld; + } else if (sizeof($retarr)>0) + break; + $rs->MoveNext(); + } + $rs->Close(); + if (empty($retarr)) $retarr = false; + + $qid = db2_primary_keys($this->_connectionID, "", $schema, $table); + if (empty($qid)) return $false; + + $rs = new ADORecordSet_db2($qid); + $ADODB_FETCH_MODE = $savem; + + if (!$rs) return $retarr; + $rs->_fetch(); + + /* + $rs->fields indices + 0 TABLE_CAT + 1 TABLE_SCHEM + 2 TABLE_NAME + 3 COLUMN_NAME + 4 KEY_SEQ + 5 PK_NAME + */ + while (!$rs->EOF) { + if (strtoupper(trim($rs->fields[2])) == $table && (!$schema || strtoupper($rs->fields[1]) == $schema)) { + $retarr[strtoupper($rs->fields[3])]->primary_key = true; + } else if (sizeof($retarr)>0) + break; + $rs->MoveNext(); + } + $rs->Close(); + + if (empty($retarr)) $retarr = false; + return $retarr; + } + + + function Prepare($sql) + { + if (! $this->_bindInputArray) return $sql; // no binding + $stmt = db2_prepare($this->_connectionID,$sql); + if (!$stmt) { + // we don't know whether db2 driver is parsing prepared stmts, so just return sql + return $sql; + } + return array($sql,$stmt,false); + } + + /* returns queryID or false */ + function _query($sql,$inputarr=false) + { + GLOBAL $php_errormsg; + if (isset($php_errormsg)) $php_errormsg = ''; + $this->_error = ''; + + if ($inputarr) { + if (is_array($sql)) { + $stmtid = $sql[1]; + } else { + $stmtid = db2_prepare($this->_connectionID,$sql); + + if ($stmtid == false) { + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + return false; + } + } + + if (! db2_execute($stmtid,$inputarr)) { + if ($this->_haserrorfunctions) { + $this->_errorMsg = db2_stmt_errormsg(); + $this->_errorCode = db2_stmt_error(); + } + return false; + } + + } else if (is_array($sql)) { + $stmtid = $sql[1]; + if (!db2_execute($stmtid)) { + if ($this->_haserrorfunctions) { + $this->_errorMsg = db2_stmt_errormsg(); + $this->_errorCode = db2_stmt_error(); + } + return false; + } + } else + $stmtid = @db2_exec($this->_connectionID,$sql); + + $this->_lastAffectedRows = 0; + if ($stmtid) { + if (@db2_num_fields($stmtid) == 0) { + $this->_lastAffectedRows = db2_num_rows($stmtid); + $stmtid = true; + } else { + $this->_lastAffectedRows = 0; + } + + if ($this->_haserrorfunctions) { + $this->_errorMsg = ''; + $this->_errorCode = 0; + } else + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + } else { + if ($this->_haserrorfunctions) { + $this->_errorMsg = db2_stmt_errormsg(); + $this->_errorCode = db2_stmt_error(); + } else + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + + } + return $stmtid; + } + + /* + Insert a null into the blob field of the table first. + Then use UpdateBlob to store the blob. + + Usage: + + $conn->Execute('INSERT INTO blobtable (id, blobcol) VALUES (1, null)'); + $conn->UpdateBlob('blobtable','blobcol',$blob,'id=1'); + */ + function UpdateBlob($table,$column,$val,$where,$blobtype='BLOB') + { + return $this->Execute("UPDATE $table SET $column=? WHERE $where",array($val)) != false; + } + + // returns true or false + function _close() + { + $ret = @db2_close($this->_connectionID); + $this->_connectionID = false; + return $ret; + } + + function _affectedrows() + { + return $this->_lastAffectedRows; + } + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordSet_db2 extends ADORecordSet { + + var $bind = false; + var $databaseType = "db2"; + var $dataProvider = "db2"; + var $useFetchArray; + + function __construct($id,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + $this->fetchMode = $mode; + + $this->_queryID = $id; + } + + + // returns the field object + function FetchField($offset = -1) + { + $o= new ADOFieldObject(); + $o->name = @db2_field_name($this->_queryID,$offset); + $o->type = @db2_field_type($this->_queryID,$offset); + $o->max_length = db2_field_width($this->_queryID,$offset); + if (ADODB_ASSOC_CASE == 0) $o->name = strtolower($o->name); + else if (ADODB_ASSOC_CASE == 1) $o->name = strtoupper($o->name); + return $o; + } + + /* Use associative array to get fields array */ + function Fields($colname) + { + if ($this->fetchMode & ADODB_FETCH_ASSOC) return $this->fields[$colname]; + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + + function _initrs() + { + global $ADODB_COUNTRECS; + $this->_numOfRows = ($ADODB_COUNTRECS) ? @db2_num_rows($this->_queryID) : -1; + $this->_numOfFields = @db2_num_fields($this->_queryID); + // some silly drivers such as db2 as/400 and intersystems cache return _numOfRows = 0 + if ($this->_numOfRows == 0) $this->_numOfRows = -1; + } + + function _seek($row) + { + return false; + } + + // speed up SelectLimit() by switching to ADODB_FETCH_NUM as ADODB_FETCH_ASSOC is emulated + function GetArrayLimit($nrows,$offset=-1) + { + if ($offset <= 0) { + $rs = $this->GetArray($nrows); + return $rs; + } + $savem = $this->fetchMode; + $this->fetchMode = ADODB_FETCH_NUM; + $this->Move($offset); + $this->fetchMode = $savem; + + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + $this->fields = $this->GetRowAssoc(); + } + + $results = array(); + $cnt = 0; + while (!$this->EOF && $nrows != $cnt) { + $results[$cnt++] = $this->fields; + $this->MoveNext(); + } + + return $results; + } + + + function MoveNext() + { + if ($this->_numOfRows != 0 && !$this->EOF) { + $this->_currentRow++; + + $this->fields = @db2_fetch_array($this->_queryID); + if ($this->fields) { + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + $this->fields = $this->GetRowAssoc(); + } + return true; + } + } + $this->fields = false; + $this->EOF = true; + return false; + } + + function _fetch() + { + + $this->fields = db2_fetch_array($this->_queryID); + if ($this->fields) { + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + $this->fields = $this->GetRowAssoc(); + } + return true; + } + $this->fields = false; + return false; + } + + function _close() + { + return @db2_free_result($this->_queryID); + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-db2oci.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-db2oci.inc.php new file mode 100644 index 000000000..91d61af14 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-db2oci.inc.php @@ -0,0 +1,226 @@ + $_COLONSZ) return $p; + $_COLONARR[] = $v; + return '?'; +} + +// smart remapping of :0, :1 bind vars to ? ? +function _colonscope($sql,$arr) +{ +global $_COLONARR,$_COLONSZ; + + $_COLONARR = array(); + $_COLONSZ = sizeof($arr); + + $sql2 = preg_replace("/(:[0-9]+)/e","_colontrack('\\1')",$sql); + + if (empty($_COLONARR)) return array($sql,$arr); + + foreach($_COLONARR as $k => $v) { + $arr2[] = $arr[$v]; + } + + return array($sql2,$arr2); +} +*/ + +/* + Smart remapping of :0, :1 bind vars to ? ? + + Handles colons in comments -- and / * * / and in quoted strings. +*/ + +function _colonparser($sql,$arr) +{ + $lensql = strlen($sql); + $arrsize = sizeof($arr); + $state = 'NORM'; + $at = 1; + $ch = $sql[0]; + $ch2 = @$sql[1]; + $sql2 = ''; + $arr2 = array(); + $nprev = 0; + + + while (strlen($ch)) { + + switch($ch) { + case '/': + if ($state == 'NORM' && $ch2 == '*') { + $state = 'COMMENT'; + + $at += 1; + $ch = $ch2; + $ch2 = $at < $lensql ? $sql[$at] : ''; + } + break; + + case '*': + if ($state == 'COMMENT' && $ch2 == '/') { + $state = 'NORM'; + + $at += 1; + $ch = $ch2; + $ch2 = $at < $lensql ? $sql[$at] : ''; + } + break; + + case "\n": + case "\r": + if ($state == 'COMMENT2') $state = 'NORM'; + break; + + case "'": + do { + $at += 1; + $ch = $ch2; + $ch2 = $at < $lensql ? $sql[$at] : ''; + } while ($ch !== "'"); + break; + + case ':': + if ($state == 'COMMENT' || $state == 'COMMENT2') break; + + //echo "$at=$ch $ch2, "; + if ('0' <= $ch2 && $ch2 <= '9') { + $n = ''; + $nat = $at; + do { + $at += 1; + $ch = $ch2; + $n .= $ch; + $ch2 = $at < $lensql ? $sql[$at] : ''; + } while ('0' <= $ch && $ch <= '9'); + #echo "$n $arrsize ] "; + $n = (integer) $n; + if ($n < $arrsize) { + $sql2 .= substr($sql,$nprev,$nat-$nprev-1).'?'; + $nprev = $at-1; + $arr2[] = $arr[$n]; + } + } + break; + + case '-': + if ($state == 'NORM') { + if ($ch2 == '-') $state = 'COMMENT2'; + $at += 1; + $ch = $ch2; + $ch2 = $at < $lensql ? $sql[$at] : ''; + } + break; + } + + $at += 1; + $ch = $ch2; + $ch2 = $at < $lensql ? $sql[$at] : ''; + } + + if ($nprev == 0) { + $sql2 = $sql; + } else { + $sql2 .= substr($sql,$nprev); + } + + return array($sql2,$arr2); +} + +class ADODB_db2oci extends ADODB_db2 { + var $databaseType = "db2oci"; + var $sysTimeStamp = 'sysdate'; + var $sysDate = 'trunc(sysdate)'; + var $_bindInputArray = true; + + function Param($name,$type='C') + { + return ':'.$name; + } + + + function MetaTables($ttype = false, $schema = false, $mask = false) + { + global $ADODB_FETCH_MODE; + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $qid = db2_tables($this->_connectionID); + + $rs = new ADORecordSet_db2($qid); + + $ADODB_FETCH_MODE = $savem; + if (!$rs) { + $false = false; + return $false; + } + + $arr = $rs->GetArray(); + $rs->Close(); + $arr2 = array(); + // adodb_pr($arr); + if ($ttype) { + $isview = strncmp($ttype,'V',1) === 0; + } + for ($i=0; $i < sizeof($arr); $i++) { + if (!$arr[$i][2]) continue; + $type = $arr[$i][3]; + $schemaval = ($schema) ? $arr[$i][1].'.' : ''; + $name = $schemaval.$arr[$i][2]; + $owner = $arr[$i][1]; + if (substr($name,0,8) == 'EXPLAIN_') continue; + if ($ttype) { + if ($isview) { + if (strncmp($type,'V',1) === 0) $arr2[] = $name; + } else if (strncmp($type,'T',1) === 0 && strncmp($owner,'SYS',3) !== 0) $arr2[] = $name; + } else if (strncmp($type,'T',1) === 0 && strncmp($owner,'SYS',3) !== 0) $arr2[] = $name; + } + return $arr2; + } + + function _Execute($sql, $inputarr=false ) + { + if ($inputarr) list($sql,$inputarr) = _colonparser($sql, $inputarr); + return parent::_Execute($sql, $inputarr); + } +}; + + +class ADORecordSet_db2oci extends ADORecordSet_db2 { + + var $databaseType = "db2oci"; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +} + +} //define diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-db2ora.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-db2ora.inc.php new file mode 100644 index 000000000..1261689db --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-db2ora.inc.php @@ -0,0 +1,86 @@ + $_COLONSZ) return $p[1]; + $_COLONARR[] = $v; + return '?'; +} + +/** + * smart remapping of :0, :1 bind vars to ? ? + * @param string $sql SQL statement + * @param array $arr parameters + * @return array + */ +function _colonscope($sql,$arr) +{ +global $_COLONARR,$_COLONSZ; + + $_COLONARR = array(); + $_COLONSZ = sizeof($arr); + + $sql2 = preg_replace_callback('/(:[0-9]+)/', '_colontrack', $sql); + + if (empty($_COLONARR)) return array($sql,$arr); + + foreach($_COLONARR as $k => $v) { + $arr2[] = $arr[$v]; + } + + return array($sql2,$arr2); +} + +class ADODB_db2oci extends ADODB_db2 { + var $databaseType = "db2oci"; + var $sysTimeStamp = 'sysdate'; + var $sysDate = 'trunc(sysdate)'; + + function _Execute($sql, $inputarr = false) + { + if ($inputarr) list($sql,$inputarr) = _colonscope($sql, $inputarr); + return parent::_Execute($sql, $inputarr); + } +}; + + +class ADORecordSet_db2oci extends ADORecordSet_odbc { + + var $databaseType = "db2oci"; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +} + +} //define diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-fbsql.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-fbsql.inc.php new file mode 100644 index 000000000..9a2440cfa --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-fbsql.inc.php @@ -0,0 +1,267 @@ +. + Set tabs to 8. +*/ + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +if (! defined("_ADODB_FBSQL_LAYER")) { + define("_ADODB_FBSQL_LAYER", 1 ); + +class ADODB_fbsql extends ADOConnection { + var $databaseType = 'fbsql'; + var $hasInsertID = true; + var $hasAffectedRows = true; + var $metaTablesSQL = "SHOW TABLES"; + var $metaColumnsSQL = "SHOW COLUMNS FROM %s"; + var $fmtTimeStamp = "'Y-m-d H:i:s'"; + var $hasLimit = false; + + function __construct() + { + } + + function _insertid() + { + return fbsql_insert_id($this->_connectionID); + } + + function _affectedrows() + { + return fbsql_affected_rows($this->_connectionID); + } + + function MetaDatabases() + { + $qid = fbsql_list_dbs($this->_connectionID); + $arr = array(); + $i = 0; + $max = fbsql_num_rows($qid); + while ($i < $max) { + $arr[] = fbsql_tablename($qid,$i); + $i += 1; + } + return $arr; + } + + // returns concatenated string + function Concat() + { + $s = ""; + $arr = func_get_args(); + $first = true; + + $s = implode(',',$arr); + if (sizeof($arr) > 0) return "CONCAT($s)"; + else return ''; + } + + // returns true or false + function _connect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + $this->_connectionID = fbsql_connect($argHostname,$argUsername,$argPassword); + if ($this->_connectionID === false) return false; + if ($argDatabasename) return $this->SelectDB($argDatabasename); + return true; + } + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + $this->_connectionID = fbsql_pconnect($argHostname,$argUsername,$argPassword); + if ($this->_connectionID === false) return false; + if ($argDatabasename) return $this->SelectDB($argDatabasename); + return true; + } + + function MetaColumns($table, $normalize=true) + { + if ($this->metaColumnsSQL) { + + $rs = $this->Execute(sprintf($this->metaColumnsSQL,$table)); + + if ($rs === false) return false; + + $retarr = array(); + while (!$rs->EOF){ + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; + $fld->type = $rs->fields[1]; + + // split type into type(length): + if (preg_match("/^(.+)\((\d+)\)$/", $fld->type, $query_array)) { + $fld->type = $query_array[1]; + $fld->max_length = $query_array[2]; + } else { + $fld->max_length = -1; + } + $fld->not_null = ($rs->fields[2] != 'YES'); + $fld->primary_key = ($rs->fields[3] == 'PRI'); + $fld->auto_increment = (strpos($rs->fields[5], 'auto_increment') !== false); + $fld->binary = (strpos($fld->type,'blob') !== false); + + $retarr[strtoupper($fld->name)] = $fld; + $rs->MoveNext(); + } + $rs->Close(); + return $retarr; + } + return false; + } + + // returns true or false + function SelectDB($dbName) + { + $this->database = $dbName; + if ($this->_connectionID) { + return @fbsql_select_db($dbName,$this->_connectionID); + } + else return false; + } + + + // returns queryID or false + function _query($sql,$inputarr=false) + { + return fbsql_query("$sql;",$this->_connectionID); + } + + /* Returns: the last error message from previous database operation */ + function ErrorMsg() + { + $this->_errorMsg = @fbsql_error($this->_connectionID); + return $this->_errorMsg; + } + + /* Returns: the last error number from previous database operation */ + function ErrorNo() + { + return @fbsql_errno($this->_connectionID); + } + + // returns true or false + function _close() + { + return @fbsql_close($this->_connectionID); + } + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordSet_fbsql extends ADORecordSet{ + + var $databaseType = "fbsql"; + var $canSeek = true; + + function __construct($queryID,$mode=false) + { + if (!$mode) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + switch ($mode) { + case ADODB_FETCH_NUM: $this->fetchMode = FBSQL_NUM; break; + case ADODB_FETCH_ASSOC: $this->fetchMode = FBSQL_ASSOC; break; + case ADODB_FETCH_BOTH: + default: + $this->fetchMode = FBSQL_BOTH; break; + } + return parent::__construct($queryID); + } + + function _initrs() + { + GLOBAL $ADODB_COUNTRECS; + $this->_numOfRows = ($ADODB_COUNTRECS) ? @fbsql_num_rows($this->_queryID):-1; + $this->_numOfFields = @fbsql_num_fields($this->_queryID); + } + + + + function FetchField($fieldOffset = -1) { + if ($fieldOffset != -1) { + $o = @fbsql_fetch_field($this->_queryID, $fieldOffset); + //$o->max_length = -1; // fbsql returns the max length less spaces -- so it is unrealiable + $f = @fbsql_field_flags($this->_queryID,$fieldOffset); + $o->binary = (strpos($f,'binary')!== false); + } + else if ($fieldOffset == -1) { /* The $fieldOffset argument is not provided thus its -1 */ + $o = @fbsql_fetch_field($this->_queryID);// fbsql returns the max length less spaces -- so it is unrealiable + //$o->max_length = -1; + } + + return $o; + } + + function _seek($row) + { + return @fbsql_data_seek($this->_queryID,$row); + } + + function _fetch($ignore_fields=false) + { + $this->fields = @fbsql_fetch_array($this->_queryID,$this->fetchMode); + return ($this->fields == true); + } + + function _close() { + return @fbsql_free_result($this->_queryID); + } + + function MetaType($t,$len=-1,$fieldobj=false) + { + if (is_object($t)) { + $fieldobj = $t; + $t = $fieldobj->type; + $len = $fieldobj->max_length; + } + $len = -1; // fbsql max_length is not accurate + switch (strtoupper($t)) { + case 'CHARACTER': + case 'CHARACTER VARYING': + case 'BLOB': + case 'CLOB': + case 'BIT': + case 'BIT VARYING': + if ($len <= $this->blobSize) return 'C'; + + // so we have to check whether binary... + case 'IMAGE': + case 'LONGBLOB': + case 'BLOB': + case 'MEDIUMBLOB': + return !empty($fieldobj->binary) ? 'B' : 'X'; + + case 'DATE': return 'D'; + + case 'TIME': + case 'TIME WITH TIME ZONE': + case 'TIMESTAMP': + case 'TIMESTAMP WITH TIME ZONE': return 'T'; + + case 'PRIMARY_KEY': + return 'R'; + case 'INTEGER': + case 'SMALLINT': + case 'BOOLEAN': + + if (!empty($fieldobj->primary_key)) return 'R'; + else return 'I'; + + default: return 'N'; + } + } + +} //class +} // defined diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-firebird.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-firebird.inc.php new file mode 100644 index 000000000..415f66f21 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-firebird.inc.php @@ -0,0 +1,73 @@ +dialect; + switch($arr['dialect']) { + case '': + case '1': $s = 'Firebird Dialect 1'; break; + case '2': $s = 'Firebird Dialect 2'; break; + default: + case '3': $s = 'Firebird Dialect 3'; break; + } + $arr['version'] = ADOConnection::_findvers($s); + $arr['description'] = $s; + return $arr; + } + + // Note that Interbase 6.5 uses this ROWS instead - don't you love forking wars! + // SELECT col1, col2 FROM table ROWS 5 -- get 5 rows + // SELECT col1, col2 FROM TABLE ORDER BY col1 ROWS 3 TO 7 -- first 5 skip 2 + function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false, $secs=0) + { + $nrows = (integer) $nrows; + $offset = (integer) $offset; + $str = 'SELECT '; + if ($nrows >= 0) $str .= "FIRST $nrows "; + $str .=($offset>=0) ? "SKIP $offset " : ''; + + $sql = preg_replace('/^[ \t]*select/i',$str,$sql); + if ($secs) + $rs = $this->CacheExecute($secs,$sql,$inputarr); + else + $rs = $this->Execute($sql,$inputarr); + + return $rs; + } + + +}; + + +class ADORecordSet_firebird extends ADORecordSet_ibase { + + var $databaseType = "firebird"; + + function __construct($id,$mode=false) + { + parent::__construct($id,$mode); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-ibase.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-ibase.inc.php new file mode 100644 index 000000000..c4f0cbdb6 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-ibase.inc.php @@ -0,0 +1,918 @@ + + changed transaction handling and added experimental blob stuff + + Docs to interbase at the website + http://www.synectics.co.za/php3/tutorial/IB_PHP3_API.html + + To use gen_id(), see + http://www.volny.cz/iprenosil/interbase/ip_ib_code.htm#_code_creategen + + $rs = $conn->Execute('select gen_id(adodb,1) from rdb$database'); + $id = $rs->fields[0]; + $conn->Execute("insert into table (id, col1,...) values ($id, $val1,...)"); +*/ + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +class ADODB_ibase extends ADOConnection { + var $databaseType = "ibase"; + var $dataProvider = "ibase"; + var $replaceQuote = "''"; // string to use to replace quotes + var $ibase_datefmt = '%Y-%m-%d'; // For hours,mins,secs change to '%Y-%m-%d %H:%M:%S'; + var $fmtDate = "'Y-m-d'"; + var $ibase_timestampfmt = "%Y-%m-%d %H:%M:%S"; + var $ibase_timefmt = "%H:%M:%S"; + var $fmtTimeStamp = "'Y-m-d, H:i:s'"; + var $concat_operator='||'; + var $_transactionID; + var $metaTablesSQL = "select rdb\$relation_name from rdb\$relations where rdb\$relation_name not like 'RDB\$%'"; + //OPN STUFF start + var $metaColumnsSQL = "select a.rdb\$field_name, a.rdb\$null_flag, a.rdb\$default_source, b.rdb\$field_length, b.rdb\$field_scale, b.rdb\$field_sub_type, b.rdb\$field_precision, b.rdb\$field_type from rdb\$relation_fields a, rdb\$fields b where a.rdb\$field_source = b.rdb\$field_name and a.rdb\$relation_name = '%s' order by a.rdb\$field_position asc"; + //OPN STUFF end + var $ibasetrans; + var $hasGenID = true; + var $_bindInputArray = true; + var $buffers = 0; + var $dialect = 1; + var $sysDate = "cast('TODAY' as timestamp)"; + var $sysTimeStamp = "CURRENT_TIMESTAMP"; //"cast('NOW' as timestamp)"; + var $ansiOuter = true; + var $hasAffectedRows = false; + var $poorAffectedRows = true; + var $blobEncodeType = 'C'; + var $role = false; + + function __construct() + { + if (defined('IBASE_DEFAULT')) $this->ibasetrans = IBASE_DEFAULT; + } + + + // returns true or false + function _connect($argHostname, $argUsername, $argPassword, $argDatabasename,$persist=false) + { + if (!function_exists('ibase_pconnect')) return null; + if ($argDatabasename) $argHostname .= ':'.$argDatabasename; + $fn = ($persist) ? 'ibase_pconnect':'ibase_connect'; + if ($this->role) + $this->_connectionID = $fn($argHostname,$argUsername,$argPassword, + $this->charSet,$this->buffers,$this->dialect,$this->role); + else + $this->_connectionID = $fn($argHostname,$argUsername,$argPassword, + $this->charSet,$this->buffers,$this->dialect); + + if ($this->dialect != 1) { // http://www.ibphoenix.com/ibp_60_del_id_ds.html + $this->replaceQuote = "''"; + } + if ($this->_connectionID === false) { + $this->_handleerror(); + return false; + } + + // PHP5 change. + if (function_exists('ibase_timefmt')) { + ibase_timefmt($this->ibase_datefmt,IBASE_DATE ); + if ($this->dialect == 1) { + ibase_timefmt($this->ibase_datefmt,IBASE_TIMESTAMP ); + } + else { + ibase_timefmt($this->ibase_timestampfmt,IBASE_TIMESTAMP ); + } + ibase_timefmt($this->ibase_timefmt,IBASE_TIME ); + + } else { + ini_set("ibase.timestampformat", $this->ibase_timestampfmt); + ini_set("ibase.dateformat", $this->ibase_datefmt); + ini_set("ibase.timeformat", $this->ibase_timefmt); + } + return true; + } + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabasename,true); + } + + + function MetaPrimaryKeys($table,$owner_notused=false,$internalKey=false) + { + if ($internalKey) { + return array('RDB$DB_KEY'); + } + + $table = strtoupper($table); + + $sql = 'SELECT S.RDB$FIELD_NAME AFIELDNAME + FROM RDB$INDICES I JOIN RDB$INDEX_SEGMENTS S ON I.RDB$INDEX_NAME=S.RDB$INDEX_NAME + WHERE I.RDB$RELATION_NAME=\''.$table.'\' and I.RDB$INDEX_NAME like \'RDB$PRIMARY%\' + ORDER BY I.RDB$INDEX_NAME,S.RDB$FIELD_POSITION'; + + $a = $this->GetCol($sql,false,true); + if ($a && sizeof($a)>0) return $a; + return false; + } + + function ServerInfo() + { + $arr['dialect'] = $this->dialect; + switch($arr['dialect']) { + case '': + case '1': $s = 'Interbase 5.5 or earlier'; break; + case '2': $s = 'Interbase 5.6'; break; + default: + case '3': $s = 'Interbase 6.0'; break; + } + $arr['version'] = ADOConnection::_findvers($s); + $arr['description'] = $s; + return $arr; + } + + function BeginTrans() + { + if ($this->transOff) return true; + $this->transCnt += 1; + $this->autoCommit = false; + $this->_transactionID = $this->_connectionID;//ibase_trans($this->ibasetrans, $this->_connectionID); + return $this->_transactionID; + } + + function CommitTrans($ok=true) + { + if (!$ok) { + return $this->RollbackTrans(); + } + if ($this->transOff) { + return true; + } + if ($this->transCnt) { + $this->transCnt -= 1; + } + $ret = false; + $this->autoCommit = true; + if ($this->_transactionID) { + //print ' commit '; + $ret = ibase_commit($this->_transactionID); + } + $this->_transactionID = false; + return $ret; + } + + // there are some compat problems with ADODB_COUNTRECS=false and $this->_logsql currently. + // it appears that ibase extension cannot support multiple concurrent queryid's + function _Execute($sql,$inputarr=false) + { + global $ADODB_COUNTRECS; + + if ($this->_logsql) { + $savecrecs = $ADODB_COUNTRECS; + $ADODB_COUNTRECS = true; // force countrecs + $ret = ADOConnection::_Execute($sql,$inputarr); + $ADODB_COUNTRECS = $savecrecs; + } else { + $ret = ADOConnection::_Execute($sql,$inputarr); + } + return $ret; + } + + function RollbackTrans() + { + if ($this->transOff) return true; + if ($this->transCnt) $this->transCnt -= 1; + $ret = false; + $this->autoCommit = true; + if ($this->_transactionID) { + $ret = ibase_rollback($this->_transactionID); + } + $this->_transactionID = false; + + return $ret; + } + + function MetaIndexes ($table, $primary = FALSE, $owner=false) + { + // save old fetch mode + global $ADODB_FETCH_MODE; + $false = false; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + $table = strtoupper($table); + $sql = "SELECT * FROM RDB\$INDICES WHERE RDB\$RELATION_NAME = '".$table."'"; + if (!$primary) { + $sql .= " AND RDB\$INDEX_NAME NOT LIKE 'RDB\$%'"; + } else { + $sql .= " AND RDB\$INDEX_NAME NOT LIKE 'RDB\$FOREIGN%'"; + } + // get index details + $rs = $this->Execute($sql); + if (!is_object($rs)) { + // restore fetchmode + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + return $false; + } + + $indexes = array(); + while ($row = $rs->FetchRow()) { + $index = $row[0]; + if (!isset($indexes[$index])) { + if (is_null($row[3])) { + $row[3] = 0; + } + $indexes[$index] = array( + 'unique' => ($row[3] == 1), + 'columns' => array() + ); + } + $sql = "SELECT * FROM RDB\$INDEX_SEGMENTS WHERE RDB\$INDEX_NAME = '".$index."' ORDER BY RDB\$FIELD_POSITION ASC"; + $rs1 = $this->Execute($sql); + while ($row1 = $rs1->FetchRow()) { + $indexes[$index]['columns'][$row1[2]] = $row1[1]; + } + } + // restore fetchmode + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + return $indexes; + } + + + // See http://community.borland.com/article/0,1410,25844,00.html + function RowLock($tables,$where,$col=false) + { + if ($this->autoCommit) { + $this->BeginTrans(); + } + $this->Execute("UPDATE $table SET $col=$col WHERE $where "); // is this correct - jlim? + return 1; + } + + + function CreateSequence($seqname = 'adodbseq', $startID = 1) + { + $ok = $this->Execute(("INSERT INTO RDB\$GENERATORS (RDB\$GENERATOR_NAME) VALUES (UPPER('$seqname'))" )); + if (!$ok) return false; + return $this->Execute("SET GENERATOR $seqname TO ".($startID-1).';'); + } + + function DropSequence($seqname = 'adodbseq') + { + $seqname = strtoupper($seqname); + $this->Execute("delete from RDB\$GENERATORS where RDB\$GENERATOR_NAME='$seqname'"); + } + + function GenID($seqname='adodbseq',$startID=1) + { + $getnext = ("SELECT Gen_ID($seqname,1) FROM RDB\$DATABASE"); + $rs = @$this->Execute($getnext); + if (!$rs) { + $this->Execute(("INSERT INTO RDB\$GENERATORS (RDB\$GENERATOR_NAME) VALUES (UPPER('$seqname'))" )); + $this->Execute("SET GENERATOR $seqname TO ".($startID-1).';'); + $rs = $this->Execute($getnext); + } + if ($rs && !$rs->EOF) { + $this->genID = (integer) reset($rs->fields); + } + else { + $this->genID = 0; // false + } + + if ($rs) { + $rs->Close(); + } + + return $this->genID; + } + + function SelectDB($dbName) + { + return false; + } + + function _handleerror() + { + $this->_errorMsg = ibase_errmsg(); + } + + function ErrorNo() + { + if (preg_match('/error code = ([\-0-9]*)/i', $this->_errorMsg,$arr)) return (integer) $arr[1]; + else return 0; + } + + function ErrorMsg() + { + return $this->_errorMsg; + } + + function Prepare($sql) + { + $stmt = ibase_prepare($this->_connectionID,$sql); + if (!$stmt) return false; + return array($sql,$stmt); + } + + // returns query ID if successful, otherwise false + // there have been reports of problems with nested queries - the code is probably not re-entrant? + function _query($sql,$iarr=false) + { + + if (!$this->autoCommit && $this->_transactionID) { + $conn = $this->_transactionID; + $docommit = false; + } else { + $conn = $this->_connectionID; + $docommit = true; + } + if (is_array($sql)) { + $fn = 'ibase_execute'; + $sql = $sql[1]; + if (is_array($iarr)) { + if (ADODB_PHPVER >= 0x4050) { // actually 4.0.4 + if ( !isset($iarr[0]) ) $iarr[0] = ''; // PHP5 compat hack + $fnarr = array_merge( array($sql) , $iarr); + $ret = call_user_func_array($fn,$fnarr); + } else { + switch(sizeof($iarr)) { + case 1: $ret = $fn($sql,$iarr[0]); break; + case 2: $ret = $fn($sql,$iarr[0],$iarr[1]); break; + case 3: $ret = $fn($sql,$iarr[0],$iarr[1],$iarr[2]); break; + case 4: $ret = $fn($sql,$iarr[0],$iarr[1],$iarr[2],$iarr[3]); break; + case 5: $ret = $fn($sql,$iarr[0],$iarr[1],$iarr[2],$iarr[3],$iarr[4]); break; + case 6: $ret = $fn($sql,$iarr[0],$iarr[1],$iarr[2],$iarr[3],$iarr[4],$iarr[5]); break; + case 7: $ret = $fn($sql,$iarr[0],$iarr[1],$iarr[2],$iarr[3],$iarr[4],$iarr[5],$iarr[6]); break; + default: ADOConnection::outp( "Too many parameters to ibase query $sql"); + case 8: $ret = $fn($sql,$iarr[0],$iarr[1],$iarr[2],$iarr[3],$iarr[4],$iarr[5],$iarr[6],$iarr[7]); break; + } + } + } else $ret = $fn($sql); + } else { + $fn = 'ibase_query'; + + if (is_array($iarr)) { + if (ADODB_PHPVER >= 0x4050) { // actually 4.0.4 + if (sizeof($iarr) == 0) $iarr[0] = ''; // PHP5 compat hack + $fnarr = array_merge( array($conn,$sql) , $iarr); + $ret = call_user_func_array($fn,$fnarr); + } else { + switch(sizeof($iarr)) { + case 1: $ret = $fn($conn,$sql,$iarr[0]); break; + case 2: $ret = $fn($conn,$sql,$iarr[0],$iarr[1]); break; + case 3: $ret = $fn($conn,$sql,$iarr[0],$iarr[1],$iarr[2]); break; + case 4: $ret = $fn($conn,$sql,$iarr[0],$iarr[1],$iarr[2],$iarr[3]); break; + case 5: $ret = $fn($conn,$sql,$iarr[0],$iarr[1],$iarr[2],$iarr[3],$iarr[4]); break; + case 6: $ret = $fn($conn,$sql,$iarr[0],$iarr[1],$iarr[2],$iarr[3],$iarr[4],$iarr[5]); break; + case 7: $ret = $fn($conn,$sql,$iarr[0],$iarr[1],$iarr[2],$iarr[3],$iarr[4],$iarr[5],$iarr[6]); break; + default: ADOConnection::outp( "Too many parameters to ibase query $sql"); + case 8: $ret = $fn($conn,$sql,$iarr[0],$iarr[1],$iarr[2],$iarr[3],$iarr[4],$iarr[5],$iarr[6],$iarr[7]); break; + } + } + } else $ret = $fn($conn,$sql); + } + if ($docommit && $ret === true) { + ibase_commit($this->_connectionID); + } + + $this->_handleerror(); + return $ret; + } + + // returns true or false + function _close() + { + if (!$this->autoCommit) { + @ibase_rollback($this->_connectionID); + } + return @ibase_close($this->_connectionID); + } + + //OPN STUFF start + function _ConvertFieldType(&$fld, $ftype, $flen, $fscale, $fsubtype, $fprecision, $dialect3) + { + $fscale = abs($fscale); + $fld->max_length = $flen; + $fld->scale = null; + switch($ftype){ + case 7: + case 8: + if ($dialect3) { + switch($fsubtype){ + case 0: + $fld->type = ($ftype == 7 ? 'smallint' : 'integer'); + break; + case 1: + $fld->type = 'numeric'; + $fld->max_length = $fprecision; + $fld->scale = $fscale; + break; + case 2: + $fld->type = 'decimal'; + $fld->max_length = $fprecision; + $fld->scale = $fscale; + break; + } // switch + } else { + if ($fscale !=0) { + $fld->type = 'decimal'; + $fld->scale = $fscale; + $fld->max_length = ($ftype == 7 ? 4 : 9); + } else { + $fld->type = ($ftype == 7 ? 'smallint' : 'integer'); + } + } + break; + case 16: + if ($dialect3) { + switch($fsubtype){ + case 0: + $fld->type = 'decimal'; + $fld->max_length = 18; + $fld->scale = 0; + break; + case 1: + $fld->type = 'numeric'; + $fld->max_length = $fprecision; + $fld->scale = $fscale; + break; + case 2: + $fld->type = 'decimal'; + $fld->max_length = $fprecision; + $fld->scale = $fscale; + break; + } // switch + } + break; + case 10: + $fld->type = 'float'; + break; + case 14: + $fld->type = 'char'; + break; + case 27: + if ($fscale !=0) { + $fld->type = 'decimal'; + $fld->max_length = 15; + $fld->scale = 5; + } else { + $fld->type = 'double'; + } + break; + case 35: + if ($dialect3) { + $fld->type = 'timestamp'; + } else { + $fld->type = 'date'; + } + break; + case 12: + $fld->type = 'date'; + break; + case 13: + $fld->type = 'time'; + break; + case 37: + $fld->type = 'varchar'; + break; + case 40: + $fld->type = 'cstring'; + break; + case 261: + $fld->type = 'blob'; + $fld->max_length = -1; + break; + } // switch + } + //OPN STUFF end + + // returns array of ADOFieldObjects for current table + function MetaColumns($table, $normalize=true) + { + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + $rs = $this->Execute(sprintf($this->metaColumnsSQL,strtoupper($table))); + + $ADODB_FETCH_MODE = $save; + $false = false; + if ($rs === false) { + return $false; + } + + $retarr = array(); + //OPN STUFF start + $dialect3 = ($this->dialect==3 ? true : false); + //OPN STUFF end + while (!$rs->EOF) { //print_r($rs->fields); + $fld = new ADOFieldObject(); + $fld->name = trim($rs->fields[0]); + //OPN STUFF start + $this->_ConvertFieldType($fld, $rs->fields[7], $rs->fields[3], $rs->fields[4], $rs->fields[5], $rs->fields[6], $dialect3); + if (isset($rs->fields[1]) && $rs->fields[1]) { + $fld->not_null = true; + } + if (isset($rs->fields[2])) { + + $fld->has_default = true; + $d = substr($rs->fields[2],strlen('default ')); + switch ($fld->type) + { + case 'smallint': + case 'integer': $fld->default_value = (int) $d; break; + case 'char': + case 'blob': + case 'text': + case 'varchar': $fld->default_value = (string) substr($d,1,strlen($d)-2); break; + case 'double': + case 'float': $fld->default_value = (float) $d; break; + default: $fld->default_value = $d; break; + } + // case 35:$tt = 'TIMESTAMP'; break; + } + if ((isset($rs->fields[5])) && ($fld->type == 'blob')) { + $fld->sub_type = $rs->fields[5]; + } else { + $fld->sub_type = null; + } + //OPN STUFF end + if ($ADODB_FETCH_MODE == ADODB_FETCH_NUM) $retarr[] = $fld; + else $retarr[strtoupper($fld->name)] = $fld; + + $rs->MoveNext(); + } + $rs->Close(); + if ( empty($retarr)) return $false; + else return $retarr; + } + + function BlobEncode( $blob ) + { + $blobid = ibase_blob_create( $this->_connectionID); + ibase_blob_add( $blobid, $blob ); + return ibase_blob_close( $blobid ); + } + + // since we auto-decode all blob's since 2.42, + // BlobDecode should not do any transforms + function BlobDecode($blob) + { + return $blob; + } + + + + + // old blobdecode function + // still used to auto-decode all blob's + function _BlobDecode_old( $blob ) + { + $blobid = ibase_blob_open($this->_connectionID, $blob ); + $realblob = ibase_blob_get( $blobid,$this->maxblobsize); // 2nd param is max size of blob -- Kevin Boillet + while($string = ibase_blob_get($blobid, 8192)){ + $realblob .= $string; + } + ibase_blob_close( $blobid ); + + return( $realblob ); + } + + function _BlobDecode( $blob ) + { + if (ADODB_PHPVER >= 0x5000) { + $blob_data = ibase_blob_info($this->_connectionID, $blob ); + $blobid = ibase_blob_open($this->_connectionID, $blob ); + } else { + + $blob_data = ibase_blob_info( $blob ); + $blobid = ibase_blob_open( $blob ); + } + + if( $blob_data[0] > $this->maxblobsize ) { + + $realblob = ibase_blob_get($blobid, $this->maxblobsize); + + while($string = ibase_blob_get($blobid, 8192)){ + $realblob .= $string; + } + } else { + $realblob = ibase_blob_get($blobid, $blob_data[0]); + } + + ibase_blob_close( $blobid ); + return( $realblob ); + } + + function UpdateBlobFile($table,$column,$path,$where,$blobtype='BLOB') + { + $fd = fopen($path,'rb'); + if ($fd === false) return false; + $blob_id = ibase_blob_create($this->_connectionID); + + /* fill with data */ + + while ($val = fread($fd,32768)){ + ibase_blob_add($blob_id, $val); + } + + /* close and get $blob_id_str for inserting into table */ + $blob_id_str = ibase_blob_close($blob_id); + + fclose($fd); + return $this->Execute("UPDATE $table SET $column=(?) WHERE $where",array($blob_id_str)) != false; + } + + /* + Insert a null into the blob field of the table first. + Then use UpdateBlob to store the blob. + + Usage: + + $conn->Execute('INSERT INTO blobtable (id, blobcol) VALUES (1, null)'); + $conn->UpdateBlob('blobtable','blobcol',$blob,'id=1'); + */ + function UpdateBlob($table,$column,$val,$where,$blobtype='BLOB') + { + $blob_id = ibase_blob_create($this->_connectionID); + + // ibase_blob_add($blob_id, $val); + + // replacement that solves the problem by which only the first modulus 64K / + // of $val are stored at the blob field //////////////////////////////////// + // Thx Abel Berenstein aberenstein#afip.gov.ar + $len = strlen($val); + $chunk_size = 32768; + $tail_size = $len % $chunk_size; + $n_chunks = ($len - $tail_size) / $chunk_size; + + for ($n = 0; $n < $n_chunks; $n++) { + $start = $n * $chunk_size; + $data = substr($val, $start, $chunk_size); + ibase_blob_add($blob_id, $data); + } + + if ($tail_size) { + $start = $n_chunks * $chunk_size; + $data = substr($val, $start, $tail_size); + ibase_blob_add($blob_id, $data); + } + // end replacement ///////////////////////////////////////////////////////// + + $blob_id_str = ibase_blob_close($blob_id); + + return $this->Execute("UPDATE $table SET $column=(?) WHERE $where",array($blob_id_str)) != false; + + } + + + function OldUpdateBlob($table,$column,$val,$where,$blobtype='BLOB') + { + $blob_id = ibase_blob_create($this->_connectionID); + ibase_blob_add($blob_id, $val); + $blob_id_str = ibase_blob_close($blob_id); + return $this->Execute("UPDATE $table SET $column=(?) WHERE $where",array($blob_id_str)) != false; + } + + // Format date column in sql string given an input format that understands Y M D + // Only since Interbase 6.0 - uses EXTRACT + // problem - does not zero-fill the day and month yet + function SQLDate($fmt, $col=false) + { + if (!$col) $col = $this->sysDate; + $s = ''; + + $len = strlen($fmt); + for ($i=0; $i < $len; $i++) { + if ($s) $s .= '||'; + $ch = $fmt[$i]; + switch($ch) { + case 'Y': + case 'y': + $s .= "extract(year from $col)"; + break; + case 'M': + case 'm': + $s .= "extract(month from $col)"; + break; + case 'Q': + case 'q': + $s .= "cast(((extract(month from $col)+2) / 3) as integer)"; + break; + case 'D': + case 'd': + $s .= "(extract(day from $col))"; + break; + case 'H': + case 'h': + $s .= "(extract(hour from $col))"; + break; + case 'I': + case 'i': + $s .= "(extract(minute from $col))"; + break; + case 'S': + case 's': + $s .= "CAST((extract(second from $col)) AS INTEGER)"; + break; + + default: + if ($ch == '\\') { + $i++; + $ch = substr($fmt,$i,1); + } + $s .= $this->qstr($ch); + break; + } + } + return $s; + } +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordset_ibase extends ADORecordSet +{ + + var $databaseType = "ibase"; + var $bind=false; + var $_cacheType; + + function __construct($id,$mode=false) + { + global $ADODB_FETCH_MODE; + + $this->fetchMode = ($mode === false) ? $ADODB_FETCH_MODE : $mode; + parent::__construct($id); + } + + /* Returns: an object containing field information. + Get column information in the Recordset object. fetchField() can be used in order to obtain information about + fields in a certain query result. If the field offset isn't specified, the next field that wasn't yet retrieved by + fetchField() is retrieved. */ + + function FetchField($fieldOffset = -1) + { + $fld = new ADOFieldObject; + $ibf = ibase_field_info($this->_queryID,$fieldOffset); + + $name = empty($ibf['alias']) ? $ibf['name'] : $ibf['alias']; + + switch (ADODB_ASSOC_CASE) { + case ADODB_ASSOC_CASE_UPPER: + $fld->name = strtoupper($name); + break; + case ADODB_ASSOC_CASE_LOWER: + $fld->name = strtolower($name); + break; + case ADODB_ASSOC_CASE_NATIVE: + default: + $fld->name = $name; + break; + } + + $fld->type = $ibf['type']; + $fld->max_length = $ibf['length']; + + /* This needs to be populated from the metadata */ + $fld->not_null = false; + $fld->has_default = false; + $fld->default_value = 'null'; + return $fld; + } + + function _initrs() + { + $this->_numOfRows = -1; + $this->_numOfFields = @ibase_num_fields($this->_queryID); + + // cache types for blob decode check + for ($i=0, $max = $this->_numOfFields; $i < $max; $i++) { + $f1 = $this->FetchField($i); + $this->_cacheType[] = $f1->type; + } + } + + function _seek($row) + { + return false; + } + + function _fetch() + { + $f = @ibase_fetch_row($this->_queryID); + if ($f === false) { + $this->fields = false; + return false; + } + // OPN stuff start - optimized + // fix missing nulls and decode blobs automatically + + global $ADODB_ANSI_PADDING_OFF; + //$ADODB_ANSI_PADDING_OFF=1; + $rtrim = !empty($ADODB_ANSI_PADDING_OFF); + + for ($i=0, $max = $this->_numOfFields; $i < $max; $i++) { + if ($this->_cacheType[$i]=="BLOB") { + if (isset($f[$i])) { + $f[$i] = $this->connection->_BlobDecode($f[$i]); + } else { + $f[$i] = null; + } + } else { + if (!isset($f[$i])) { + $f[$i] = null; + } else if ($rtrim && is_string($f[$i])) { + $f[$i] = rtrim($f[$i]); + } + } + } + // OPN stuff end + + $this->fields = $f; + if ($this->fetchMode == ADODB_FETCH_ASSOC) { + $this->fields = $this->GetRowAssoc(); + } else if ($this->fetchMode == ADODB_FETCH_BOTH) { + $this->fields = array_merge($this->fields,$this->GetRowAssoc()); + } + return true; + } + + /* Use associative array to get fields array */ + function Fields($colname) + { + if ($this->fetchMode & ADODB_FETCH_ASSOC) return $this->fields[$colname]; + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + + } + + + function _close() + { + return @ibase_free_result($this->_queryID); + } + + function MetaType($t,$len=-1,$fieldobj=false) + { + if (is_object($t)) { + $fieldobj = $t; + $t = $fieldobj->type; + $len = $fieldobj->max_length; + } + switch (strtoupper($t)) { + case 'CHAR': + return 'C'; + + case 'TEXT': + case 'VARCHAR': + case 'VARYING': + if ($len <= $this->blobSize) return 'C'; + return 'X'; + case 'BLOB': + return 'B'; + + case 'TIMESTAMP': + case 'DATE': return 'D'; + case 'TIME': return 'T'; + //case 'T': return 'T'; + + //case 'L': return 'L'; + case 'INT': + case 'SHORT': + case 'INTEGER': return 'I'; + default: return 'N'; + } + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-informix.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-informix.inc.php new file mode 100644 index 000000000..801451ea5 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-informix.inc.php @@ -0,0 +1,41 @@ + + +*/ + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +if (!defined('IFX_SCROLL')) define('IFX_SCROLL',1); + +class ADODB_informix72 extends ADOConnection { + var $databaseType = "informix72"; + var $dataProvider = "informix"; + var $replaceQuote = "''"; // string to use to replace quotes + var $fmtDate = "'Y-m-d'"; + var $fmtTimeStamp = "'Y-m-d H:i:s'"; + var $hasInsertID = true; + var $hasAffectedRows = true; + var $substr = 'substr'; + var $metaTablesSQL="select tabname,tabtype from systables where tabtype in ('T','V') and owner!='informix'"; //Don't get informix tables and pseudo-tables + + + var $metaColumnsSQL = + "select c.colname, c.coltype, c.collength, d.default,c.colno + from syscolumns c, systables t,outer sysdefaults d + where c.tabid=t.tabid and d.tabid=t.tabid and d.colno=c.colno + and tabname='%s' order by c.colno"; + + var $metaPrimaryKeySQL = + "select part1,part2,part3,part4,part5,part6,part7,part8 from + systables t,sysconstraints s,sysindexes i where t.tabname='%s' + and s.tabid=t.tabid and s.constrtype='P' + and i.idxname=s.idxname"; + + var $concat_operator = '||'; + + var $lastQuery = false; + var $has_insertid = true; + + var $_autocommit = true; + var $_bindInputArray = true; // set to true if ADOConnection.Execute() permits binding of array parameters. + var $sysDate = 'TODAY'; + var $sysTimeStamp = 'CURRENT'; + var $cursorType = IFX_SCROLL; // IFX_SCROLL or IFX_HOLD or 0 + + function __construct() + { + // alternatively, use older method: + //putenv("DBDATE=Y4MD-"); + + // force ISO date format + putenv('GL_DATE=%Y-%m-%d'); + + if (function_exists('ifx_byteasvarchar')) { + ifx_byteasvarchar(1); // Mode "0" will return a blob id, and mode "1" will return a varchar with text content. + ifx_textasvarchar(1); // Mode "0" will return a blob id, and mode "1" will return a varchar with text content. + ifx_blobinfile_mode(0); // Mode "0" means save Byte-Blobs in memory, and mode "1" means save Byte-Blobs in a file. + } + } + + function ServerInfo() + { + if (isset($this->version)) return $this->version; + + $arr['description'] = $this->GetOne("select DBINFO('version','full') from systables where tabid = 1"); + $arr['version'] = $this->GetOne("select DBINFO('version','major') || DBINFO('version','minor') from systables where tabid = 1"); + $this->version = $arr; + return $arr; + } + + + + function _insertid() + { + $sqlca =ifx_getsqlca($this->lastQuery); + return @$sqlca["sqlerrd1"]; + } + + function _affectedrows() + { + if ($this->lastQuery) { + return @ifx_affected_rows ($this->lastQuery); + } + return 0; + } + + function BeginTrans() + { + if ($this->transOff) return true; + $this->transCnt += 1; + $this->Execute('BEGIN'); + $this->_autocommit = false; + return true; + } + + function CommitTrans($ok=true) + { + if (!$ok) return $this->RollbackTrans(); + if ($this->transOff) return true; + if ($this->transCnt) $this->transCnt -= 1; + $this->Execute('COMMIT'); + $this->_autocommit = true; + return true; + } + + function RollbackTrans() + { + if ($this->transOff) return true; + if ($this->transCnt) $this->transCnt -= 1; + $this->Execute('ROLLBACK'); + $this->_autocommit = true; + return true; + } + + function RowLock($tables,$where,$col='1 as adodbignore') + { + if ($this->_autocommit) $this->BeginTrans(); + return $this->GetOne("select $col from $tables where $where for update"); + } + + /* Returns: the last error message from previous database operation + Note: This function is NOT available for Microsoft SQL Server. */ + + function ErrorMsg() + { + if (!empty($this->_logsql)) return $this->_errorMsg; + $this->_errorMsg = ifx_errormsg(); + return $this->_errorMsg; + } + + function ErrorNo() + { + preg_match("/.*SQLCODE=([^\]]*)/",ifx_error(),$parse); + if (is_array($parse) && isset($parse[1])) return (int)$parse[1]; + return 0; + } + + + function MetaProcedures($NamePattern = false, $catalog = null, $schemaPattern = null) + { + // save old fetch mode + global $ADODB_FETCH_MODE; + + $false = false; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + + } + $procedures = array (); + + // get index details + + $likepattern = ''; + if ($NamePattern) { + $likepattern = " WHERE procname LIKE '".$NamePattern."'"; + } + + $rs = $this->Execute('SELECT procname, isproc FROM sysprocedures'.$likepattern); + + if (is_object($rs)) { + // parse index data into array + + while ($row = $rs->FetchRow()) { + $procedures[$row[0]] = array( + 'type' => ($row[1] == 'f' ? 'FUNCTION' : 'PROCEDURE'), + 'catalog' => '', + 'schema' => '', + 'remarks' => '' + ); + } + } + + // restore fetchmode + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + return $procedures; + } + + function MetaColumns($table, $normalize=true) + { + global $ADODB_FETCH_MODE; + + $false = false; + if (!empty($this->metaColumnsSQL)) { + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== false) $savem = $this->SetFetchMode(false); + $rs = $this->Execute(sprintf($this->metaColumnsSQL,$table)); + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + if ($rs === false) return $false; + $rspkey = $this->Execute(sprintf($this->metaPrimaryKeySQL,$table)); //Added to get primary key colno items + + $retarr = array(); + while (!$rs->EOF) { //print_r($rs->fields); + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; +/* //!eos. + $rs->fields[1] is not the correct adodb type + $rs->fields[2] is not correct max_length, because can include not-null bit + + $fld->type = $rs->fields[1]; + $fld->primary_key=$rspkey->fields && array_search($rs->fields[4],$rspkey->fields); //Added to set primary key flag + $fld->max_length = $rs->fields[2];*/ + $pr=ifx_props($rs->fields[1],$rs->fields[2]); //!eos + $fld->type = $pr[0] ;//!eos + $fld->primary_key=$rspkey->fields && array_search($rs->fields[4],$rspkey->fields); + $fld->max_length = $pr[1]; //!eos + $fld->precision = $pr[2] ;//!eos + $fld->not_null = $pr[3]=="N"; //!eos + + if (trim($rs->fields[3]) != "AAAAAA 0") { + $fld->has_default = 1; + $fld->default_value = $rs->fields[3]; + } else { + $fld->has_default = 0; + } + + $retarr[strtolower($fld->name)] = $fld; + $rs->MoveNext(); + } + + $rs->Close(); + $rspkey->Close(); //!eos + return $retarr; + } + + return $false; + } + + function xMetaColumns($table) + { + return ADOConnection::MetaColumns($table,false); + } + + function MetaForeignKeys($table, $owner=false, $upper=false) //!Eos + { + $sql = " + select tr.tabname,updrule,delrule, + i.part1 o1,i2.part1 d1,i.part2 o2,i2.part2 d2,i.part3 o3,i2.part3 d3,i.part4 o4,i2.part4 d4, + i.part5 o5,i2.part5 d5,i.part6 o6,i2.part6 d6,i.part7 o7,i2.part7 d7,i.part8 o8,i2.part8 d8 + from systables t,sysconstraints s,sysindexes i, + sysreferences r,systables tr,sysconstraints s2,sysindexes i2 + where t.tabname='$table' + and s.tabid=t.tabid and s.constrtype='R' and r.constrid=s.constrid + and i.idxname=s.idxname and tr.tabid=r.ptabid + and s2.constrid=r.primary and i2.idxname=s2.idxname"; + + $rs = $this->Execute($sql); + if (!$rs || $rs->EOF) return false; + $arr = $rs->GetArray(); + $a = array(); + foreach($arr as $v) { + $coldest=$this->metaColumnNames($v["tabname"]); + $colorig=$this->metaColumnNames($table); + $colnames=array(); + for($i=1;$i<=8 && $v["o$i"] ;$i++) { + $colnames[]=$coldest[$v["d$i"]-1]."=".$colorig[$v["o$i"]-1]; + } + if($upper) + $a[strtoupper($v["tabname"])] = $colnames; + else + $a[$v["tabname"]] = $colnames; + } + return $a; + } + + function UpdateBlob($table, $column, $val, $where, $blobtype = 'BLOB') + { + $type = ($blobtype == 'TEXT') ? 1 : 0; + $blobid = ifx_create_blob($type,0,$val); + return $this->Execute("UPDATE $table SET $column=(?) WHERE $where",array($blobid)); + } + + function BlobDecode($blobid) + { + return function_exists('ifx_byteasvarchar') ? $blobid : @ifx_get_blob($blobid); + } + + // returns true or false + function _connect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + if (!function_exists('ifx_connect')) return null; + + $dbs = $argDatabasename . "@" . $argHostname; + if ($argHostname) putenv("INFORMIXSERVER=$argHostname"); + putenv("INFORMIXSERVER=".trim($argHostname)); + $this->_connectionID = ifx_connect($dbs,$argUsername,$argPassword); + if ($this->_connectionID === false) return false; + #if ($argDatabasename) return $this->SelectDB($argDatabasename); + return true; + } + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + if (!function_exists('ifx_connect')) return null; + + $dbs = $argDatabasename . "@" . $argHostname; + putenv("INFORMIXSERVER=".trim($argHostname)); + $this->_connectionID = ifx_pconnect($dbs,$argUsername,$argPassword); + if ($this->_connectionID === false) return false; + #if ($argDatabasename) return $this->SelectDB($argDatabasename); + return true; + } +/* + // ifx_do does not accept bind parameters - weird ??? + function Prepare($sql) + { + $stmt = ifx_prepare($sql); + if (!$stmt) return $sql; + else return array($sql,$stmt); + } +*/ + // returns query ID if successful, otherwise false + function _query($sql,$inputarr=false) + { + global $ADODB_COUNTRECS; + + // String parameters have to be converted using ifx_create_char + if ($inputarr) { + foreach($inputarr as $v) { + if (gettype($v) == 'string') { + $tab[] = ifx_create_char($v); + } + else { + $tab[] = $v; + } + } + } + + // In case of select statement, we use a scroll cursor in order + // to be able to call "move", or "movefirst" statements + if (!$ADODB_COUNTRECS && preg_match("/^\s*select/is", $sql)) { + if ($inputarr) { + $this->lastQuery = ifx_query($sql,$this->_connectionID, $this->cursorType, $tab); + } + else { + $this->lastQuery = ifx_query($sql,$this->_connectionID, $this->cursorType); + } + } + else { + if ($inputarr) { + $this->lastQuery = ifx_query($sql,$this->_connectionID, $tab); + } + else { + $this->lastQuery = ifx_query($sql,$this->_connectionID); + } + } + + // Following line have been commented because autocommit mode is + // not supported by informix SE 7.2 + + //if ($this->_autocommit) ifx_query('COMMIT',$this->_connectionID); + + return $this->lastQuery; + } + + // returns true or false + function _close() + { + $this->lastQuery = false; + if($this->_connectionID) { + return ifx_close($this->_connectionID); + } + return true; + } +} + + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordset_informix72 extends ADORecordSet { + + var $databaseType = "informix72"; + var $canSeek = true; + var $_fieldprops = false; + + function __construct($id,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + $this->fetchMode = $mode; + return parent::__construct($id); + } + + + + /* Returns: an object containing field information. + Get column information in the Recordset object. fetchField() can be used in order to obtain information about + fields in a certain query result. If the field offset isn't specified, the next field that wasn't yet retrieved by + fetchField() is retrieved. */ + function FetchField($fieldOffset = -1) + { + if (empty($this->_fieldprops)) { + $fp = ifx_fieldproperties($this->_queryID); + foreach($fp as $k => $v) { + $o = new ADOFieldObject; + $o->name = $k; + $arr = explode(';',$v); //"SQLTYPE;length;precision;scale;ISNULLABLE" + $o->type = $arr[0]; + $o->max_length = $arr[1]; + $this->_fieldprops[] = $o; + $o->not_null = $arr[4]=="N"; + } + } + $ret = $this->_fieldprops[$fieldOffset]; + return $ret; + } + + function _initrs() + { + $this->_numOfRows = -1; // ifx_affected_rows not reliable, only returns estimate -- ($ADODB_COUNTRECS)? ifx_affected_rows($this->_queryID):-1; + $this->_numOfFields = ifx_num_fields($this->_queryID); + } + + function _seek($row) + { + return @ifx_fetch_row($this->_queryID, (int) $row); + } + + function MoveLast() + { + $this->fields = @ifx_fetch_row($this->_queryID, "LAST"); + if ($this->fields) $this->EOF = false; + $this->_currentRow = -1; + + if ($this->fetchMode == ADODB_FETCH_NUM) { + foreach($this->fields as $v) { + $arr[] = $v; + } + $this->fields = $arr; + } + + return true; + } + + function MoveFirst() + { + $this->fields = @ifx_fetch_row($this->_queryID, "FIRST"); + if ($this->fields) $this->EOF = false; + $this->_currentRow = 0; + + if ($this->fetchMode == ADODB_FETCH_NUM) { + foreach($this->fields as $v) { + $arr[] = $v; + } + $this->fields = $arr; + } + + return true; + } + + function _fetch($ignore_fields=false) + { + + $this->fields = @ifx_fetch_row($this->_queryID); + + if (!is_array($this->fields)) return false; + + if ($this->fetchMode == ADODB_FETCH_NUM) { + foreach($this->fields as $v) { + $arr[] = $v; + } + $this->fields = $arr; + } + return true; + } + + /* close() only needs to be called if you are worried about using too much memory while your script + is running. All associated result memory for the specified result identifier will automatically be freed. */ + function _close() + { + if($this->_queryID) { + return ifx_free_result($this->_queryID); + } + return true; + } + +} +/** !Eos +* Auxiliar function to Parse coltype,collength. Used by Metacolumns +* return: array ($mtype,$length,$precision,$nullable) (similar to ifx_fieldpropierties) +*/ +function ifx_props($coltype,$collength){ + $itype=fmod($coltype+1,256); + $nullable=floor(($coltype+1) /256) ?"N":"Y"; + $mtype=substr(" CIIFFNNDN TBXCC ",$itype,1); + switch ($itype){ + case 2: + $length=4; + case 6: + case 9: + case 14: + $length=floor($collength/256); + $precision=fmod($collength,256); + break; + default: + $precision=0; + $length=$collength; + } + return array($mtype,$length,$precision,$nullable); +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-ldap.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-ldap.inc.php new file mode 100644 index 000000000..633705e09 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-ldap.inc.php @@ -0,0 +1,428 @@ +_connectionID = @ldap_connect($host); + } else { + $conn_info = array( $host,$this->port); + + if ( strstr( $host, ':' ) ) { + $conn_info = explode( ':', $host ); + } + + $this->_connectionID = @ldap_connect( $conn_info[0], $conn_info[1] ); + } + if (!$this->_connectionID) { + $e = 'Could not connect to ' . $conn_info[0]; + $this->_errorMsg = $e; + if ($this->debug) ADOConnection::outp($e); + return false; + } + if( count( $LDAP_CONNECT_OPTIONS ) > 0 ) { + $this->_inject_bind_options( $LDAP_CONNECT_OPTIONS ); + } + + if ($username) { + $bind = @ldap_bind( $this->_connectionID, $username, $password ); + } else { + $username = 'anonymous'; + $bind = @ldap_bind( $this->_connectionID ); + } + + if (!$bind) { + $e = sprintf($this->_bind_errmsg,ldap_error($this->_connectionID)); + $this->_errorMsg = $e; + if ($this->debug) ADOConnection::outp($e); + return false; + } + $this->_errorMsg = ''; + $this->database = $ldapbase; + return $this->_connectionID; + } + +/* + Valid Domain Values for LDAP Options: + + LDAP_OPT_DEREF (integer) + LDAP_OPT_SIZELIMIT (integer) + LDAP_OPT_TIMELIMIT (integer) + LDAP_OPT_PROTOCOL_VERSION (integer) + LDAP_OPT_ERROR_NUMBER (integer) + LDAP_OPT_REFERRALS (boolean) + LDAP_OPT_RESTART (boolean) + LDAP_OPT_HOST_NAME (string) + LDAP_OPT_ERROR_STRING (string) + LDAP_OPT_MATCHED_DN (string) + LDAP_OPT_SERVER_CONTROLS (array) + LDAP_OPT_CLIENT_CONTROLS (array) + + Make sure to set this BEFORE calling Connect() + + Example: + + $LDAP_CONNECT_OPTIONS = Array( + Array ( + "OPTION_NAME"=>LDAP_OPT_DEREF, + "OPTION_VALUE"=>2 + ), + Array ( + "OPTION_NAME"=>LDAP_OPT_SIZELIMIT, + "OPTION_VALUE"=>100 + ), + Array ( + "OPTION_NAME"=>LDAP_OPT_TIMELIMIT, + "OPTION_VALUE"=>30 + ), + Array ( + "OPTION_NAME"=>LDAP_OPT_PROTOCOL_VERSION, + "OPTION_VALUE"=>3 + ), + Array ( + "OPTION_NAME"=>LDAP_OPT_ERROR_NUMBER, + "OPTION_VALUE"=>13 + ), + Array ( + "OPTION_NAME"=>LDAP_OPT_REFERRALS, + "OPTION_VALUE"=>FALSE + ), + Array ( + "OPTION_NAME"=>LDAP_OPT_RESTART, + "OPTION_VALUE"=>FALSE + ) + ); +*/ + + function _inject_bind_options( $options ) { + foreach( $options as $option ) { + ldap_set_option( $this->_connectionID, $option["OPTION_NAME"], $option["OPTION_VALUE"] ) + or die( "Unable to set server option: " . $option["OPTION_NAME"] ); + } + } + + /* returns _queryID or false */ + function _query($sql,$inputarr=false) + { + $rs = @ldap_search( $this->_connectionID, $this->database, $sql ); + $this->_errorMsg = ($rs) ? '' : 'Search error on '.$sql.': '.ldap_error($this->_connectionID); + return $rs; + } + + function ErrorMsg() + { + return $this->_errorMsg; + } + + function ErrorNo() + { + return @ldap_errno($this->_connectionID); + } + + /* closes the LDAP connection */ + function _close() + { + @ldap_close( $this->_connectionID ); + $this->_connectionID = false; + } + + function SelectDB($db) { + $this->database = $db; + return true; + } // SelectDB + + function ServerInfo() + { + if( !empty( $this->version ) ) { + return $this->version; + } + + $version = array(); + /* + Determines how aliases are handled during search. + LDAP_DEREF_NEVER (0x00) + LDAP_DEREF_SEARCHING (0x01) + LDAP_DEREF_FINDING (0x02) + LDAP_DEREF_ALWAYS (0x03) + The LDAP_DEREF_SEARCHING value means aliases are dereferenced during the search but + not when locating the base object of the search. The LDAP_DEREF_FINDING value means + aliases are dereferenced when locating the base object but not during the search. + Default: LDAP_DEREF_NEVER + */ + ldap_get_option( $this->_connectionID, LDAP_OPT_DEREF, $version['LDAP_OPT_DEREF'] ) ; + switch ( $version['LDAP_OPT_DEREF'] ) { + case 0: + $version['LDAP_OPT_DEREF'] = 'LDAP_DEREF_NEVER'; + case 1: + $version['LDAP_OPT_DEREF'] = 'LDAP_DEREF_SEARCHING'; + case 2: + $version['LDAP_OPT_DEREF'] = 'LDAP_DEREF_FINDING'; + case 3: + $version['LDAP_OPT_DEREF'] = 'LDAP_DEREF_ALWAYS'; + } + + /* + A limit on the number of entries to return from a search. + LDAP_NO_LIMIT (0) means no limit. + Default: LDAP_NO_LIMIT + */ + ldap_get_option( $this->_connectionID, LDAP_OPT_SIZELIMIT, $version['LDAP_OPT_SIZELIMIT'] ); + if ( $version['LDAP_OPT_SIZELIMIT'] == 0 ) { + $version['LDAP_OPT_SIZELIMIT'] = 'LDAP_NO_LIMIT'; + } + + /* + A limit on the number of seconds to spend on a search. + LDAP_NO_LIMIT (0) means no limit. + Default: LDAP_NO_LIMIT + */ + ldap_get_option( $this->_connectionID, LDAP_OPT_TIMELIMIT, $version['LDAP_OPT_TIMELIMIT'] ); + if ( $version['LDAP_OPT_TIMELIMIT'] == 0 ) { + $version['LDAP_OPT_TIMELIMIT'] = 'LDAP_NO_LIMIT'; + } + + /* + Determines whether the LDAP library automatically follows referrals returned by LDAP servers or not. + LDAP_OPT_ON + LDAP_OPT_OFF + Default: ON + */ + ldap_get_option( $this->_connectionID, LDAP_OPT_REFERRALS, $version['LDAP_OPT_REFERRALS'] ); + if ( $version['LDAP_OPT_REFERRALS'] == 0 ) { + $version['LDAP_OPT_REFERRALS'] = 'LDAP_OPT_OFF'; + } else { + $version['LDAP_OPT_REFERRALS'] = 'LDAP_OPT_ON'; + } + + /* + Determines whether LDAP I/O operations are automatically restarted if they abort prematurely. + LDAP_OPT_ON + LDAP_OPT_OFF + Default: OFF + */ + ldap_get_option( $this->_connectionID, LDAP_OPT_RESTART, $version['LDAP_OPT_RESTART'] ); + if ( $version['LDAP_OPT_RESTART'] == 0 ) { + $version['LDAP_OPT_RESTART'] = 'LDAP_OPT_OFF'; + } else { + $version['LDAP_OPT_RESTART'] = 'LDAP_OPT_ON'; + } + + /* + This option indicates the version of the LDAP protocol used when communicating with the primary LDAP server. + LDAP_VERSION2 (2) + LDAP_VERSION3 (3) + Default: LDAP_VERSION2 (2) + */ + ldap_get_option( $this->_connectionID, LDAP_OPT_PROTOCOL_VERSION, $version['LDAP_OPT_PROTOCOL_VERSION'] ); + if ( $version['LDAP_OPT_PROTOCOL_VERSION'] == 2 ) { + $version['LDAP_OPT_PROTOCOL_VERSION'] = 'LDAP_VERSION2'; + } else { + $version['LDAP_OPT_PROTOCOL_VERSION'] = 'LDAP_VERSION3'; + } + + /* The host name (or list of hosts) for the primary LDAP server. */ + ldap_get_option( $this->_connectionID, LDAP_OPT_HOST_NAME, $version['LDAP_OPT_HOST_NAME'] ); + ldap_get_option( $this->_connectionID, LDAP_OPT_ERROR_NUMBER, $version['LDAP_OPT_ERROR_NUMBER'] ); + ldap_get_option( $this->_connectionID, LDAP_OPT_ERROR_STRING, $version['LDAP_OPT_ERROR_STRING'] ); + ldap_get_option( $this->_connectionID, LDAP_OPT_MATCHED_DN, $version['LDAP_OPT_MATCHED_DN'] ); + + return $this->version = $version; + } +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordSet_ldap extends ADORecordSet{ + + var $databaseType = "ldap"; + var $canSeek = false; + var $_entryID; /* keeps track of the entry resource identifier */ + + function __construct($queryID,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + switch ($mode) + { + case ADODB_FETCH_NUM: + $this->fetchMode = LDAP_NUM; + break; + case ADODB_FETCH_ASSOC: + $this->fetchMode = LDAP_ASSOC; + break; + case ADODB_FETCH_DEFAULT: + case ADODB_FETCH_BOTH: + default: + $this->fetchMode = LDAP_BOTH; + break; + } + + parent::__construct($queryID); + } + + function _initrs() + { + /* + This could be teaked to respect the $COUNTRECS directive from ADODB + It's currently being used in the _fetch() function and the + GetAssoc() function + */ + $this->_numOfRows = ldap_count_entries( $this->connection->_connectionID, $this->_queryID ); + } + + /* + Return whole recordset as a multi-dimensional associative array + */ + function GetAssoc($force_array = false, $first2cols = false) + { + $records = $this->_numOfRows; + $results = array(); + for ( $i=0; $i < $records; $i++ ) { + foreach ( $this->fields as $k=>$v ) { + if ( is_array( $v ) ) { + if ( $v['count'] == 1 ) { + $results[$i][$k] = $v[0]; + } else { + array_shift( $v ); + $results[$i][$k] = $v; + } + } + } + } + + return $results; + } + + function GetRowAssoc($upper = ADODB_ASSOC_CASE) + { + $results = array(); + foreach ( $this->fields as $k=>$v ) { + if ( is_array( $v ) ) { + if ( $v['count'] == 1 ) { + $results[$k] = $v[0]; + } else { + array_shift( $v ); + $results[$k] = $v; + } + } + } + + return $results; + } + + function GetRowNums() + { + $results = array(); + foreach ( $this->fields as $k=>$v ) { + static $i = 0; + if (is_array( $v )) { + if ( $v['count'] == 1 ) { + $results[$i] = $v[0]; + } else { + array_shift( $v ); + $results[$i] = $v; + } + $i++; + } + } + return $results; + } + + function _fetch() + { + if ( $this->_currentRow >= $this->_numOfRows && $this->_numOfRows >= 0 ) { + return false; + } + + if ( $this->_currentRow == 0 ) { + $this->_entryID = ldap_first_entry( $this->connection->_connectionID, $this->_queryID ); + } else { + $this->_entryID = ldap_next_entry( $this->connection->_connectionID, $this->_entryID ); + } + + $this->fields = ldap_get_attributes( $this->connection->_connectionID, $this->_entryID ); + $this->_numOfFields = $this->fields['count']; + + switch ( $this->fetchMode ) { + + case LDAP_ASSOC: + $this->fields = $this->GetRowAssoc(); + break; + + case LDAP_NUM: + $this->fields = array_merge($this->GetRowNums(),$this->GetRowAssoc()); + break; + + case LDAP_BOTH: + default: + $this->fields = $this->GetRowNums(); + break; + } + + return is_array( $this->fields ); + } + + function _close() { + @ldap_free_result( $this->_queryID ); + $this->_queryID = false; + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-mssql.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-mssql.inc.php new file mode 100644 index 000000000..068dffce0 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-mssql.inc.php @@ -0,0 +1,1178 @@ += 0x4300) { +// docs say 4.2.0, but testing shows only since 4.3.0 does it work! + ini_set('mssql.datetimeconvert',0); +} else { +global $ADODB_mssql_mths; // array, months must be upper-case + + + $ADODB_mssql_date_order = 'mdy'; + $ADODB_mssql_mths = array( + 'JAN'=>1,'FEB'=>2,'MAR'=>3,'APR'=>4,'MAY'=>5,'JUN'=>6, + 'JUL'=>7,'AUG'=>8,'SEP'=>9,'OCT'=>10,'NOV'=>11,'DEC'=>12); +} + +//--------------------------------------------------------------------------- +// Call this to autoset $ADODB_mssql_date_order at the beginning of your code, +// just after you connect to the database. Supports mdy and dmy only. +// Not required for PHP 4.2.0 and above. +function AutoDetect_MSSQL_Date_Order($conn) +{ +global $ADODB_mssql_date_order; + $adate = $conn->GetOne('select getdate()'); + if ($adate) { + $anum = (int) $adate; + if ($anum > 0) { + if ($anum > 31) { + //ADOConnection::outp( "MSSQL: YYYY-MM-DD date format not supported currently"); + } else + $ADODB_mssql_date_order = 'dmy'; + } else + $ADODB_mssql_date_order = 'mdy'; + } +} + +class ADODB_mssql extends ADOConnection { + var $databaseType = "mssql"; + var $dataProvider = "mssql"; + var $replaceQuote = "''"; // string to use to replace quotes + var $fmtDate = "'Y-m-d'"; + var $fmtTimeStamp = "'Y-m-d\TH:i:s'"; + var $hasInsertID = true; + var $substr = "substring"; + var $length = 'len'; + var $hasAffectedRows = true; + var $metaDatabasesSQL = "select name from sysdatabases where name <> 'master'"; + var $metaTablesSQL="select name,case when type='U' then 'T' else 'V' end from sysobjects where (type='U' or type='V') and (name not in ('sysallocations','syscolumns','syscomments','sysdepends','sysfilegroups','sysfiles','sysfiles1','sysforeignkeys','sysfulltextcatalogs','sysindexes','sysindexkeys','sysmembers','sysobjects','syspermissions','sysprotects','sysreferences','systypes','sysusers','sysalternates','sysconstraints','syssegments','REFERENTIAL_CONSTRAINTS','CHECK_CONSTRAINTS','CONSTRAINT_TABLE_USAGE','CONSTRAINT_COLUMN_USAGE','VIEWS','VIEW_TABLE_USAGE','VIEW_COLUMN_USAGE','SCHEMATA','TABLES','TABLE_CONSTRAINTS','TABLE_PRIVILEGES','COLUMNS','COLUMN_DOMAIN_USAGE','COLUMN_PRIVILEGES','DOMAINS','DOMAIN_CONSTRAINTS','KEY_COLUMN_USAGE','dtproperties'))"; + var $metaColumnsSQL = # xtype==61 is datetime + "select c.name,t.name,c.length,c.isnullable, c.status, + (case when c.xusertype=61 then 0 else c.xprec end), + (case when c.xusertype=61 then 0 else c.xscale end) + from syscolumns c join systypes t on t.xusertype=c.xusertype join sysobjects o on o.id=c.id where o.name='%s'"; + var $hasTop = 'top'; // support mssql SELECT TOP 10 * FROM TABLE + var $hasGenID = true; + var $sysDate = 'convert(datetime,convert(char,GetDate(),102),102)'; + var $sysTimeStamp = 'GetDate()'; + var $_has_mssql_init; + var $maxParameterLen = 4000; + var $arrayClass = 'ADORecordSet_array_mssql'; + var $uniqueSort = true; + var $leftOuter = '*='; + var $rightOuter = '=*'; + var $ansiOuter = true; // for mssql7 or later + var $poorAffectedRows = true; + var $identitySQL = 'select SCOPE_IDENTITY()'; // 'select SCOPE_IDENTITY'; # for mssql 2000 + var $uniqueOrderBy = true; + var $_bindInputArray = true; + var $forceNewConnect = false; + + function __construct() + { + $this->_has_mssql_init = (strnatcmp(PHP_VERSION,'4.1.0')>=0); + } + + function ServerInfo() + { + global $ADODB_FETCH_MODE; + + + if ($this->fetchMode === false) { + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + } else + $savem = $this->SetFetchMode(ADODB_FETCH_NUM); + + if (0) { + $stmt = $this->PrepareSP('sp_server_info'); + $val = 2; + $this->Parameter($stmt,$val,'attribute_id'); + $row = $this->GetRow($stmt); + } + + $row = $this->GetRow("execute sp_server_info 2"); + + + if ($this->fetchMode === false) { + $ADODB_FETCH_MODE = $savem; + } else + $this->SetFetchMode($savem); + + $arr['description'] = $row[2]; + $arr['version'] = ADOConnection::_findvers($arr['description']); + return $arr; + } + + function IfNull( $field, $ifNull ) + { + return " ISNULL($field, $ifNull) "; // if MS SQL Server + } + + function _insertid() + { + // SCOPE_IDENTITY() + // Returns the last IDENTITY value inserted into an IDENTITY column in + // the same scope. A scope is a module -- a stored procedure, trigger, + // function, or batch. Thus, two statements are in the same scope if + // they are in the same stored procedure, function, or batch. + if ($this->lastInsID !== false) { + return $this->lastInsID; // InsID from sp_executesql call + } else { + return $this->GetOne($this->identitySQL); + } + } + + + + /** + * Correctly quotes a string so that all strings are escaped. We prefix and append + * to the string single-quotes. + * An example is $db->qstr("Don't bother",magic_quotes_runtime()); + * + * @param s the string to quote + * @param [magic_quotes] if $s is GET/POST var, set to get_magic_quotes_gpc(). + * This undoes the stupidity of magic quotes for GPC. + * + * @return quoted string to be sent back to database + */ + function qstr($s,$magic_quotes=false) + { + if (!$magic_quotes) { + return "'".str_replace("'",$this->replaceQuote,$s)."'"; + } + + // undo magic quotes for " unless sybase is on + $sybase = ini_get('magic_quotes_sybase'); + if (!$sybase) { + $s = str_replace('\\"','"',$s); + if ($this->replaceQuote == "\\'") // ' already quoted, no need to change anything + return "'$s'"; + else {// change \' to '' for sybase/mssql + $s = str_replace('\\\\','\\',$s); + return "'".str_replace("\\'",$this->replaceQuote,$s)."'"; + } + } else { + return "'".$s."'"; + } + } +// moodle change end - see readme_moodle.txt + + function _affectedrows() + { + return $this->GetOne('select @@rowcount'); + } + + var $_dropSeqSQL = "drop table %s"; + + function CreateSequence($seq='adodbseq',$start=1) + { + + $this->Execute('BEGIN TRANSACTION adodbseq'); + $start -= 1; + $this->Execute("create table $seq (id float(53))"); + $ok = $this->Execute("insert into $seq with (tablock,holdlock) values($start)"); + if (!$ok) { + $this->Execute('ROLLBACK TRANSACTION adodbseq'); + return false; + } + $this->Execute('COMMIT TRANSACTION adodbseq'); + return true; + } + + function GenID($seq='adodbseq',$start=1) + { + //$this->debug=1; + $this->Execute('BEGIN TRANSACTION adodbseq'); + $ok = $this->Execute("update $seq with (tablock,holdlock) set id = id + 1"); + if (!$ok) { + $this->Execute("create table $seq (id float(53))"); + $ok = $this->Execute("insert into $seq with (tablock,holdlock) values($start)"); + if (!$ok) { + $this->Execute('ROLLBACK TRANSACTION adodbseq'); + return false; + } + $this->Execute('COMMIT TRANSACTION adodbseq'); + return $start; + } + $num = $this->GetOne("select id from $seq"); + $this->Execute('COMMIT TRANSACTION adodbseq'); + return $num; + + // in old implementation, pre 1.90, we returned GUID... + //return $this->GetOne("SELECT CONVERT(varchar(255), NEWID()) AS 'Char'"); + } + + + function SelectLimit($sql,$nrows=-1,$offset=-1, $inputarr=false,$secs2cache=0) + { + if ($nrows > 0 && $offset <= 0) { + $sql = preg_replace( + '/(^\s*select\s+(distinctrow|distinct)?)/i','\\1 '.$this->hasTop." $nrows ",$sql); + + if ($secs2cache) + $rs = $this->CacheExecute($secs2cache, $sql, $inputarr); + else + $rs = $this->Execute($sql,$inputarr); + } else + $rs = ADOConnection::SelectLimit($sql,$nrows,$offset,$inputarr,$secs2cache); + + return $rs; + } + + + // Format date column in sql string given an input format that understands Y M D + function SQLDate($fmt, $col=false) + { + if (!$col) $col = $this->sysTimeStamp; + $s = ''; + + $len = strlen($fmt); + for ($i=0; $i < $len; $i++) { + if ($s) $s .= '+'; + $ch = $fmt[$i]; + switch($ch) { + case 'Y': + case 'y': + $s .= "datename(yyyy,$col)"; + break; + case 'M': + $s .= "convert(char(3),$col,0)"; + break; + case 'm': + $s .= "replace(str(month($col),2),' ','0')"; + break; + case 'Q': + case 'q': + $s .= "datename(quarter,$col)"; + break; + case 'D': + case 'd': + $s .= "replace(str(day($col),2),' ','0')"; + break; + case 'h': + $s .= "substring(convert(char(14),$col,0),13,2)"; + break; + + case 'H': + $s .= "replace(str(datepart(hh,$col),2),' ','0')"; + break; + + case 'i': + $s .= "replace(str(datepart(mi,$col),2),' ','0')"; + break; + case 's': + $s .= "replace(str(datepart(ss,$col),2),' ','0')"; + break; + case 'a': + case 'A': + $s .= "substring(convert(char(19),$col,0),18,2)"; + break; + + default: + if ($ch == '\\') { + $i++; + $ch = substr($fmt,$i,1); + } + $s .= $this->qstr($ch); + break; + } + } + return $s; + } + + + function BeginTrans() + { + if ($this->transOff) return true; + $this->transCnt += 1; + $ok = $this->Execute('BEGIN TRAN'); + return $ok; + } + + function CommitTrans($ok=true) + { + if ($this->transOff) return true; + if (!$ok) return $this->RollbackTrans(); + if ($this->transCnt) $this->transCnt -= 1; + $ok = $this->Execute('COMMIT TRAN'); + return $ok; + } + function RollbackTrans() + { + if ($this->transOff) return true; + if ($this->transCnt) $this->transCnt -= 1; + $ok = $this->Execute('ROLLBACK TRAN'); + return $ok; + } + + function SetTransactionMode( $transaction_mode ) + { + $this->_transmode = $transaction_mode; + if (empty($transaction_mode)) { + $this->Execute('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); + return; + } + if (!stristr($transaction_mode,'isolation')) $transaction_mode = 'ISOLATION LEVEL '.$transaction_mode; + $this->Execute("SET TRANSACTION ".$transaction_mode); + } + + /* + Usage: + + $this->BeginTrans(); + $this->RowLock('table1,table2','table1.id=33 and table2.id=table1.id'); # lock row 33 for both tables + + # some operation on both tables table1 and table2 + + $this->CommitTrans(); + + See http://www.swynk.com/friends/achigrik/SQL70Locks.asp + */ + function RowLock($tables,$where,$col='1 as adodbignore') + { + if ($col == '1 as adodbignore') $col = 'top 1 null as ignore'; + if (!$this->transCnt) $this->BeginTrans(); + return $this->GetOne("select $col from $tables with (ROWLOCK,HOLDLOCK) where $where"); + } + + + function MetaColumns($table, $normalize=true) + { +// $arr = ADOConnection::MetaColumns($table); +// return $arr; + + $this->_findschema($table,$schema); + if ($schema) { + $dbName = $this->database; + $this->SelectDB($schema); + } + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + if ($this->fetchMode !== false) $savem = $this->SetFetchMode(false); + $rs = $this->Execute(sprintf($this->metaColumnsSQL,$table)); + + if ($schema) { + $this->SelectDB($dbName); + } + + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + if (!is_object($rs)) { + $false = false; + return $false; + } + + $retarr = array(); + while (!$rs->EOF){ + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; + $fld->type = $rs->fields[1]; + + $fld->not_null = (!$rs->fields[3]); + $fld->auto_increment = ($rs->fields[4] == 128); // sys.syscolumns status field. 0x80 = 128 ref: http://msdn.microsoft.com/en-us/library/ms186816.aspx + + if (isset($rs->fields[5]) && $rs->fields[5]) { + if ($rs->fields[5]>0) $fld->max_length = $rs->fields[5]; + $fld->scale = $rs->fields[6]; + if ($fld->scale>0) $fld->max_length += 1; + } else + $fld->max_length = $rs->fields[2]; + + if ($save == ADODB_FETCH_NUM) { + $retarr[] = $fld; + } else { + $retarr[strtoupper($fld->name)] = $fld; + } + $rs->MoveNext(); + } + + $rs->Close(); + return $retarr; + + } + + + function MetaIndexes($table,$primary=false, $owner=false) + { + $table = $this->qstr($table); + + $sql = "SELECT i.name AS ind_name, C.name AS col_name, USER_NAME(O.uid) AS Owner, c.colid, k.Keyno, + CASE WHEN I.indid BETWEEN 1 AND 254 AND (I.status & 2048 = 2048 OR I.Status = 16402 AND O.XType = 'V') THEN 1 ELSE 0 END AS IsPK, + CASE WHEN I.status & 2 = 2 THEN 1 ELSE 0 END AS IsUnique + FROM dbo.sysobjects o INNER JOIN dbo.sysindexes I ON o.id = i.id + INNER JOIN dbo.sysindexkeys K ON I.id = K.id AND I.Indid = K.Indid + INNER JOIN dbo.syscolumns c ON K.id = C.id AND K.colid = C.Colid + WHERE LEFT(i.name, 8) <> '_WA_Sys_' AND o.status >= 0 AND O.Name LIKE $table + ORDER BY O.name, I.Name, K.keyno"; + + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + + $rs = $this->Execute($sql); + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + if (!is_object($rs)) { + return FALSE; + } + + $indexes = array(); + while ($row = $rs->FetchRow()) { + if ($primary && !$row[5]) continue; + + $indexes[$row[0]]['unique'] = $row[6]; + $indexes[$row[0]]['columns'][] = $row[1]; + } + return $indexes; + } + + function MetaForeignKeys($table, $owner=false, $upper=false) + { + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $table = $this->qstr(strtoupper($table)); + + $sql = +"select object_name(constid) as constraint_name, + col_name(fkeyid, fkey) as column_name, + object_name(rkeyid) as referenced_table_name, + col_name(rkeyid, rkey) as referenced_column_name +from sysforeignkeys +where upper(object_name(fkeyid)) = $table +order by constraint_name, referenced_table_name, keyno"; + + $constraints = $this->GetArray($sql); + + $ADODB_FETCH_MODE = $save; + + $arr = false; + foreach($constraints as $constr) { + //print_r($constr); + $arr[$constr[0]][$constr[2]][] = $constr[1].'='.$constr[3]; + } + if (!$arr) return false; + + $arr2 = false; + + foreach($arr as $k => $v) { + foreach($v as $a => $b) { + if ($upper) $a = strtoupper($a); + $arr2[$a] = $b; + } + } + return $arr2; + } + + //From: Fernando Moreira + function MetaDatabases() + { + if(@mssql_select_db("master")) { + $qry=$this->metaDatabasesSQL; + if($rs=@mssql_query($qry,$this->_connectionID)){ + $tmpAr=$ar=array(); + while($tmpAr=@mssql_fetch_row($rs)) + $ar[]=$tmpAr[0]; + @mssql_select_db($this->database); + if(sizeof($ar)) + return($ar); + else + return(false); + } else { + @mssql_select_db($this->database); + return(false); + } + } + return(false); + } + + // "Stein-Aksel Basma" + // tested with MSSQL 2000 + function MetaPrimaryKeys($table, $owner=false) + { + global $ADODB_FETCH_MODE; + + $schema = ''; + $this->_findschema($table,$schema); + if (!$schema) $schema = $this->database; + if ($schema) $schema = "and k.table_catalog like '$schema%'"; + + $sql = "select distinct k.column_name,ordinal_position from information_schema.key_column_usage k, + information_schema.table_constraints tc + where tc.constraint_name = k.constraint_name and tc.constraint_type = + 'PRIMARY KEY' and k.table_name = '$table' $schema order by ordinal_position "; + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $a = $this->GetCol($sql); + $ADODB_FETCH_MODE = $savem; + + if ($a && sizeof($a)>0) return $a; + $false = false; + return $false; + } + + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + if ($mask) { + $save = $this->metaTablesSQL; + $mask = $this->qstr(($mask)); + $this->metaTablesSQL .= " AND name like $mask"; + } + $ret = ADOConnection::MetaTables($ttype,$showSchema); + + if ($mask) { + $this->metaTablesSQL = $save; + } + return $ret; + } + + function SelectDB($dbName) + { + $this->database = $dbName; + $this->databaseName = $dbName; # obsolete, retained for compat with older adodb versions + if ($this->_connectionID) { + return @mssql_select_db($dbName); + } + else return false; + } + + function ErrorMsg() + { + if (empty($this->_errorMsg)){ + $this->_errorMsg = mssql_get_last_message(); + } + return $this->_errorMsg; + } + + function ErrorNo() + { + if ($this->_logsql && $this->_errorCode !== false) return $this->_errorCode; + if (empty($this->_errorMsg)) { + $this->_errorMsg = mssql_get_last_message(); + } + $id = @mssql_query("select @@ERROR",$this->_connectionID); + if (!$id) return false; + $arr = mssql_fetch_array($id); + @mssql_free_result($id); + if (is_array($arr)) return $arr[0]; + else return -1; + } + + // returns true or false, newconnect supported since php 5.1.0. + function _connect($argHostname, $argUsername, $argPassword, $argDatabasename,$newconnect=false) + { + if (!function_exists('mssql_pconnect')) return null; + $this->_connectionID = mssql_connect($argHostname,$argUsername,$argPassword,$newconnect); + if ($this->_connectionID === false) return false; + if ($argDatabasename) return $this->SelectDB($argDatabasename); + return true; + } + + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + if (!function_exists('mssql_pconnect')) return null; + $this->_connectionID = mssql_pconnect($argHostname,$argUsername,$argPassword); + if ($this->_connectionID === false) return false; + + // persistent connections can forget to rollback on crash, so we do it here. + if ($this->autoRollback) { + $cnt = $this->GetOne('select @@TRANCOUNT'); + while (--$cnt >= 0) $this->Execute('ROLLBACK TRAN'); + } + if ($argDatabasename) return $this->SelectDB($argDatabasename); + return true; + } + + function _nconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabasename, true); + } + + function Prepare($sql) + { + $sqlarr = explode('?',$sql); + if (sizeof($sqlarr) <= 1) return $sql; + $sql2 = $sqlarr[0]; + for ($i = 1, $max = sizeof($sqlarr); $i < $max; $i++) { + $sql2 .= '@P'.($i-1) . $sqlarr[$i]; + } + return array($sql,$this->qstr($sql2),$max,$sql2); + } + + function PrepareSP($sql,$param=true) + { + if (!$this->_has_mssql_init) { + ADOConnection::outp( "PrepareSP: mssql_init only available since PHP 4.1.0"); + return $sql; + } + $stmt = mssql_init($sql,$this->_connectionID); + if (!$stmt) return $sql; + return array($sql,$stmt); + } + + // returns concatenated string + // MSSQL requires integers to be cast as strings + // automatically cast every datatype to VARCHAR(255) + // @author David Rogers (introspectshun) + function Concat() + { + $s = ""; + $arr = func_get_args(); + + // Split single record on commas, if possible + if (sizeof($arr) == 1) { + foreach ($arr as $arg) { + $args = explode(',', $arg); + } + $arr = $args; + } + + array_walk($arr, create_function('&$v', '$v = "CAST(" . $v . " AS VARCHAR(255))";')); + $s = implode('+',$arr); + if (sizeof($arr) > 0) return "$s"; + + return ''; + } + + /* + Usage: + $stmt = $db->PrepareSP('SP_RUNSOMETHING'); -- takes 2 params, @myid and @group + + # note that the parameter does not have @ in front! + $db->Parameter($stmt,$id,'myid'); + $db->Parameter($stmt,$group,'group',false,64); + $db->Execute($stmt); + + @param $stmt Statement returned by Prepare() or PrepareSP(). + @param $var PHP variable to bind to. Can set to null (for isNull support). + @param $name Name of stored procedure variable name to bind to. + @param [$isOutput] Indicates direction of parameter 0/false=IN 1=OUT 2= IN/OUT. This is ignored in oci8. + @param [$maxLen] Holds an maximum length of the variable. + @param [$type] The data type of $var. Legal values depend on driver. + + See mssql_bind documentation at php.net. + */ + function Parameter(&$stmt, &$var, $name, $isOutput=false, $maxLen=4000, $type=false) + { + if (!$this->_has_mssql_init) { + ADOConnection::outp( "Parameter: mssql_bind only available since PHP 4.1.0"); + return false; + } + + $isNull = is_null($var); // php 4.0.4 and above... + + if ($type === false) + switch(gettype($var)) { + default: + case 'string': $type = SQLVARCHAR; break; + case 'double': $type = SQLFLT8; break; + case 'integer': $type = SQLINT4; break; + case 'boolean': $type = SQLINT1; break; # SQLBIT not supported in 4.1.0 + } + + if ($this->debug) { + $prefix = ($isOutput) ? 'Out' : 'In'; + $ztype = (empty($type)) ? 'false' : $type; + ADOConnection::outp( "{$prefix}Parameter(\$stmt, \$php_var='$var', \$name='$name', \$maxLen=$maxLen, \$type=$ztype);"); + } + /* + See http://phplens.com/lens/lensforum/msgs.php?id=7231 + + RETVAL is HARD CODED into php_mssql extension: + The return value (a long integer value) is treated like a special OUTPUT parameter, + called "RETVAL" (without the @). See the example at mssql_execute to + see how it works. - type: one of this new supported PHP constants. + SQLTEXT, SQLVARCHAR,SQLCHAR, SQLINT1,SQLINT2, SQLINT4, SQLBIT,SQLFLT8 + */ + if ($name !== 'RETVAL') $name = '@'.$name; + return mssql_bind($stmt[1], $name, $var, $type, $isOutput, $isNull, $maxLen); + } + + /* + Unfortunately, it appears that mssql cannot handle varbinary > 255 chars + So all your blobs must be of type "image". + + Remember to set in php.ini the following... + + ; Valid range 0 - 2147483647. Default = 4096. + mssql.textlimit = 0 ; zero to pass through + + ; Valid range 0 - 2147483647. Default = 4096. + mssql.textsize = 0 ; zero to pass through + */ + function UpdateBlob($table,$column,$val,$where,$blobtype='BLOB') + { + + if (strtoupper($blobtype) == 'CLOB') { + $sql = "UPDATE $table SET $column='" . $val . "' WHERE $where"; + return $this->Execute($sql) != false; + } + $sql = "UPDATE $table SET $column=0x".bin2hex($val)." WHERE $where"; + return $this->Execute($sql) != false; + } + + // returns query ID if successful, otherwise false + function _query($sql,$inputarr=false) + { + $this->_errorMsg = false; + if (is_array($inputarr)) { + + # bind input params with sp_executesql: + # see http://www.quest-pipelines.com/newsletter-v3/0402_F.htm + # works only with sql server 7 and newer + $getIdentity = false; + if (!is_array($sql) && preg_match('/^\\s*insert/i', $sql)) { + $getIdentity = true; + $sql .= (preg_match('/;\\s*$/i', $sql) ? ' ' : '; ') . $this->identitySQL; + } + if (!is_array($sql)) $sql = $this->Prepare($sql); + $params = ''; + $decl = ''; + $i = 0; + foreach($inputarr as $v) { + if ($decl) { + $decl .= ', '; + $params .= ', '; + } + if (is_string($v)) { + $len = strlen($v); + if ($len == 0) $len = 1; + + if ($len > 4000 ) { + // NVARCHAR is max 4000 chars. Let's use NTEXT + $decl .= "@P$i NTEXT"; + } else { + $decl .= "@P$i NVARCHAR($len)"; + } + + $params .= "@P$i=N". (strncmp($v,"'",1)==0? $v : $this->qstr($v)); + } else if (is_integer($v)) { + $decl .= "@P$i INT"; + $params .= "@P$i=".$v; + } else if (is_float($v)) { + $decl .= "@P$i FLOAT"; + $params .= "@P$i=".$v; + } else if (is_bool($v)) { + $decl .= "@P$i INT"; # Used INT just in case BIT in not supported on the user's MSSQL version. It will cast appropriately. + $params .= "@P$i=".(($v)?'1':'0'); # True == 1 in MSSQL BIT fields and acceptable for storing logical true in an int field + } else { + $decl .= "@P$i CHAR"; # Used char because a type is required even when the value is to be NULL. + $params .= "@P$i=NULL"; + } + $i += 1; + } + $decl = $this->qstr($decl); + if ($this->debug) ADOConnection::outp("sp_executesql N{$sql[1]},N$decl,$params"); + $rez = mssql_query("sp_executesql N{$sql[1]},N$decl,$params", $this->_connectionID); + if ($getIdentity) { + $arr = @mssql_fetch_row($rez); + $this->lastInsID = isset($arr[0]) ? $arr[0] : false; + @mssql_data_seek($rez, 0); + } + + } else if (is_array($sql)) { + # PrepareSP() + $rez = mssql_execute($sql[1]); + $this->lastInsID = false; + + } else { + $rez = mssql_query($sql,$this->_connectionID); + $this->lastInsID = false; + } + return $rez; + } + + // returns true or false + function _close() + { + if ($this->transCnt) $this->RollbackTrans(); + $rez = @mssql_close($this->_connectionID); + $this->_connectionID = false; + return $rez; + } + + // mssql uses a default date like Dec 30 2000 12:00AM + static function UnixDate($v) + { + return ADORecordSet_array_mssql::UnixDate($v); + } + + static function UnixTimeStamp($v) + { + return ADORecordSet_array_mssql::UnixTimeStamp($v); + } +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordset_mssql extends ADORecordSet { + + var $databaseType = "mssql"; + var $canSeek = true; + var $hasFetchAssoc; // see http://phplens.com/lens/lensforum/msgs.php?id=6083 + // _mths works only in non-localised system + + function __construct($id,$mode=false) + { + // freedts check... + $this->hasFetchAssoc = function_exists('mssql_fetch_assoc'); + + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + + } + $this->fetchMode = $mode; + return parent::__construct($id,$mode); + } + + + function _initrs() + { + GLOBAL $ADODB_COUNTRECS; + $this->_numOfRows = ($ADODB_COUNTRECS)? @mssql_num_rows($this->_queryID):-1; + $this->_numOfFields = @mssql_num_fields($this->_queryID); + } + + + //Contributed by "Sven Axelsson" + // get next resultset - requires PHP 4.0.5 or later + function NextRecordSet() + { + if (!mssql_next_result($this->_queryID)) return false; + $this->_inited = false; + $this->bind = false; + $this->_currentRow = -1; + $this->Init(); + return true; + } + + /* Use associative array to get fields array */ + function Fields($colname) + { + if ($this->fetchMode != ADODB_FETCH_NUM) return $this->fields[$colname]; + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + /* Returns: an object containing field information. + Get column information in the Recordset object. fetchField() can be used in order to obtain information about + fields in a certain query result. If the field offset isn't specified, the next field that wasn't yet retrieved by + fetchField() is retrieved. */ + + function FetchField($fieldOffset = -1) + { + if ($fieldOffset != -1) { + $f = @mssql_fetch_field($this->_queryID, $fieldOffset); + } + else if ($fieldOffset == -1) { /* The $fieldOffset argument is not provided thus its -1 */ + $f = @mssql_fetch_field($this->_queryID); + } + $false = false; + if (empty($f)) return $false; + return $f; + } + + function _seek($row) + { + return @mssql_data_seek($this->_queryID, $row); + } + + // speedup + function MoveNext() + { + if ($this->EOF) return false; + + $this->_currentRow++; + + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + if ($this->fetchMode & ADODB_FETCH_NUM) { + //ADODB_FETCH_BOTH mode + $this->fields = @mssql_fetch_array($this->_queryID); + } + else { + if ($this->hasFetchAssoc) {// only for PHP 4.2.0 or later + $this->fields = @mssql_fetch_assoc($this->_queryID); + } else { + $flds = @mssql_fetch_array($this->_queryID); + if (is_array($flds)) { + $fassoc = array(); + foreach($flds as $k => $v) { + if (is_numeric($k)) continue; + $fassoc[$k] = $v; + } + $this->fields = $fassoc; + } else + $this->fields = false; + } + } + + if (is_array($this->fields)) { + if (ADODB_ASSOC_CASE == 0) { + foreach($this->fields as $k=>$v) { + $kn = strtolower($k); + if ($kn <> $k) { + unset($this->fields[$k]); + $this->fields[$kn] = $v; + } + } + } else if (ADODB_ASSOC_CASE == 1) { + foreach($this->fields as $k=>$v) { + $kn = strtoupper($k); + if ($kn <> $k) { + unset($this->fields[$k]); + $this->fields[$kn] = $v; + } + } + } + } + } else { + $this->fields = @mssql_fetch_row($this->_queryID); + } + if ($this->fields) return true; + $this->EOF = true; + + return false; + } + + + // INSERT UPDATE DELETE returns false even if no error occurs in 4.0.4 + // also the date format has been changed from YYYY-mm-dd to dd MMM YYYY in 4.0.4. Idiot! + function _fetch($ignore_fields=false) + { + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + if ($this->fetchMode & ADODB_FETCH_NUM) { + //ADODB_FETCH_BOTH mode + $this->fields = @mssql_fetch_array($this->_queryID); + } else { + if ($this->hasFetchAssoc) // only for PHP 4.2.0 or later + $this->fields = @mssql_fetch_assoc($this->_queryID); + else { + $this->fields = @mssql_fetch_array($this->_queryID); + if (@is_array($$this->fields)) { + $fassoc = array(); + foreach($$this->fields as $k => $v) { + if (is_integer($k)) continue; + $fassoc[$k] = $v; + } + $this->fields = $fassoc; + } + } + } + + if (!$this->fields) { + } else if (ADODB_ASSOC_CASE == 0) { + foreach($this->fields as $k=>$v) { + $kn = strtolower($k); + if ($kn <> $k) { + unset($this->fields[$k]); + $this->fields[$kn] = $v; + } + } + } else if (ADODB_ASSOC_CASE == 1) { + foreach($this->fields as $k=>$v) { + $kn = strtoupper($k); + if ($kn <> $k) { + unset($this->fields[$k]); + $this->fields[$kn] = $v; + } + } + } + } else { + $this->fields = @mssql_fetch_row($this->_queryID); + } + return $this->fields; + } + + /* close() only needs to be called if you are worried about using too much memory while your script + is running. All associated result memory for the specified result identifier will automatically be freed. */ + + function _close() + { + if($this->_queryID) { + $rez = mssql_free_result($this->_queryID); + $this->_queryID = false; + return $rez; + } + return true; + } + + // mssql uses a default date like Dec 30 2000 12:00AM + static function UnixDate($v) + { + return ADORecordSet_array_mssql::UnixDate($v); + } + + static function UnixTimeStamp($v) + { + return ADORecordSet_array_mssql::UnixTimeStamp($v); + } + +} + + +class ADORecordSet_array_mssql extends ADORecordSet_array { + function __construct($id=-1,$mode=false) + { + parent::__construct($id,$mode); + } + + // mssql uses a default date like Dec 30 2000 12:00AM + static function UnixDate($v) + { + + if (is_numeric(substr($v,0,1)) && ADODB_PHPVER >= 0x4200) return parent::UnixDate($v); + + global $ADODB_mssql_mths,$ADODB_mssql_date_order; + + //Dec 30 2000 12:00AM + if ($ADODB_mssql_date_order == 'dmy') { + if (!preg_match( "|^([0-9]{1,2})[-/\. ]+([A-Za-z]{3})[-/\. ]+([0-9]{4})|" ,$v, $rr)) { + return parent::UnixDate($v); + } + if ($rr[3] <= TIMESTAMP_FIRST_YEAR) return 0; + + $theday = $rr[1]; + $themth = substr(strtoupper($rr[2]),0,3); + } else { + if (!preg_match( "|^([A-Za-z]{3})[-/\. ]+([0-9]{1,2})[-/\. ]+([0-9]{4})|" ,$v, $rr)) { + return parent::UnixDate($v); + } + if ($rr[3] <= TIMESTAMP_FIRST_YEAR) return 0; + + $theday = $rr[2]; + $themth = substr(strtoupper($rr[1]),0,3); + } + $themth = $ADODB_mssql_mths[$themth]; + if ($themth <= 0) return false; + // h-m-s-MM-DD-YY + return mktime(0,0,0,$themth,$theday,$rr[3]); + } + + static function UnixTimeStamp($v) + { + + if (is_numeric(substr($v,0,1)) && ADODB_PHPVER >= 0x4200) return parent::UnixTimeStamp($v); + + global $ADODB_mssql_mths,$ADODB_mssql_date_order; + + //Dec 30 2000 12:00AM + if ($ADODB_mssql_date_order == 'dmy') { + if (!preg_match( "|^([0-9]{1,2})[-/\. ]+([A-Za-z]{3})[-/\. ]+([0-9]{4}) +([0-9]{1,2}):([0-9]{1,2}) *([apAP]{0,1})|" + ,$v, $rr)) return parent::UnixTimeStamp($v); + if ($rr[3] <= TIMESTAMP_FIRST_YEAR) return 0; + + $theday = $rr[1]; + $themth = substr(strtoupper($rr[2]),0,3); + } else { + if (!preg_match( "|^([A-Za-z]{3})[-/\. ]+([0-9]{1,2})[-/\. ]+([0-9]{4}) +([0-9]{1,2}):([0-9]{1,2}) *([apAP]{0,1})|" + ,$v, $rr)) return parent::UnixTimeStamp($v); + if ($rr[3] <= TIMESTAMP_FIRST_YEAR) return 0; + + $theday = $rr[2]; + $themth = substr(strtoupper($rr[1]),0,3); + } + + $themth = $ADODB_mssql_mths[$themth]; + if ($themth <= 0) return false; + + switch (strtoupper($rr[6])) { + case 'P': + if ($rr[4]<12) $rr[4] += 12; + break; + case 'A': + if ($rr[4]==12) $rr[4] = 0; + break; + default: + break; + } + // h-m-s-MM-DD-YY + return mktime($rr[4],$rr[5],0,$themth,$theday,$rr[3]); + } +} + +/* +Code Example 1: + +select object_name(constid) as constraint_name, + object_name(fkeyid) as table_name, + col_name(fkeyid, fkey) as column_name, + object_name(rkeyid) as referenced_table_name, + col_name(rkeyid, rkey) as referenced_column_name +from sysforeignkeys +where object_name(fkeyid) = x +order by constraint_name, table_name, referenced_table_name, keyno + +Code Example 2: +select constraint_name, + column_name, + ordinal_position +from information_schema.key_column_usage +where constraint_catalog = db_name() +and table_name = x +order by constraint_name, ordinal_position + +http://www.databasejournal.com/scripts/article.php/1440551 +*/ diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-mssql_n.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-mssql_n.inc.php new file mode 100644 index 000000000..2bbac5b28 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-mssql_n.inc.php @@ -0,0 +1,249 @@ +_appendN($sql); + return ADODB_mssql::_query($sql,$inputarr); + } + + /** + * This function will intercept all the literals used in the SQL, prepending the "N" char to them + * in order to allow mssql to store properly data sent in the correct UCS-2 encoding (by freeTDS + * and ODBTP) keeping SQL compatibility at ADOdb level (instead of hacking every project to add + * the "N" notation when working against MSSQL. + * + * The orginal note indicated that this hack should only be used if ALL the char-based columns + * in your DB are of type nchar, nvarchar and ntext, but testing seems to indicate that SQL server + * doesn't seem to care if the statement is used against char etc fields. + * + * @todo This function should raise an ADOdb error if one of the transformations fail + * + * @param mixed $inboundData Either a string containing an SQL statement + * or an array with resources from prepared statements + * + * @return mixed + */ + function _appendN($inboundData) { + + $inboundIsArray = false; + + if (is_array($inboundData)) + { + $inboundIsArray = true; + $inboundArray = $inboundData; + } else + $inboundArray = (array)$inboundData; + + /* + * All changes will be placed here + */ + $outboundArray = $inboundArray; + + foreach($inboundArray as $inboundKey=>$inboundValue) + { + + if (is_resource($inboundValue)) + { + /* + * Prepared statement resource + */ + if ($this->debug) + ADOConnection::outp("{$this->databaseType} index $inboundKey value is resource, continue"); + + continue; + } + + if (strpos($inboundValue, SINGLEQUOTE) === false) + { + /* + * Check we have something to manipulate + */ + if ($this->debug) + ADOConnection::outp("{$this->databaseType} index $inboundKey value $inboundValue has no single quotes, continue"); + continue; + } + + /* + * Check we haven't an odd number of single quotes (this can cause problems below + * and should be considered one wrong SQL). Exit with debug info. + */ + if ((substr_count($inboundValue, SINGLEQUOTE) & 1)) + { + if ($this->debug) + ADOConnection::outp("{$this->databaseType} internal transformation: not converted. Wrong number of quotes (odd)"); + + break; + } + + /* + * Check we haven't any backslash + single quote combination. It should mean wrong + * backslashes use (bad magic_quotes_sybase?). Exit with debug info. + */ + $regexp = '/(\\\\' . SINGLEQUOTE . '[^' . SINGLEQUOTE . '])/'; + if (preg_match($regexp, $inboundValue)) + { + if ($this->debug) + ADOConnection::outp("{$this->databaseType} internal transformation: not converted. Found bad use of backslash + single quote"); + + break; + } + + /* + * Remove pairs of single-quotes + */ + $pairs = array(); + $regexp = '/(' . SINGLEQUOTE . SINGLEQUOTE . ')/'; + preg_match_all($regexp, $inboundValue, $list_of_pairs); + + if ($list_of_pairs) + { + foreach (array_unique($list_of_pairs[0]) as $key=>$value) + $pairs['<@#@#@PAIR-'.$key.'@#@#@>'] = $value; + + + if (!empty($pairs)) + $inboundValue = str_replace($pairs, array_keys($pairs), $inboundValue); + + } + + /* + * Remove the rest of literals present in the query + */ + $literals = array(); + $regexp = '/(N?' . SINGLEQUOTE . '.*?' . SINGLEQUOTE . ')/is'; + preg_match_all($regexp, $inboundValue, $list_of_literals); + + if ($list_of_literals) + { + foreach (array_unique($list_of_literals[0]) as $key=>$value) + $literals['<#@#@#LITERAL-'.$key.'#@#@#>'] = $value; + + + if (!empty($literals)) + $inboundValue = str_replace($literals, array_keys($literals), $inboundValue); + } + + /* + * Analyse literals to prepend the N char to them if their contents aren't numeric + */ + if (!empty($literals)) + { + foreach ($literals as $key=>$value) { + if (!is_numeric(trim($value, SINGLEQUOTE))) + /* + * Non numeric string, prepend our dear N, whilst + * Trimming potentially existing previous "N" + */ + $literals[$key] = 'N' . trim($value, 'N'); + + } + } + + /* + * Re-apply literals to the text + */ + if (!empty($literals)) + $inboundValue = str_replace(array_keys($literals), $literals, $inboundValue); + + + /* + * Any pairs followed by N' must be switched to N' followed by those pairs + * (or strings beginning with single quotes will fail) + */ + $inboundValue = preg_replace("/((<@#@#@PAIR-(\d+)@#@#@>)+)N'/", "N'$1", $inboundValue); + + /* + * Re-apply pairs of single-quotes to the text + */ + if (!empty($pairs)) + $inboundValue = str_replace(array_keys($pairs), $pairs, $inboundValue); + + + /* + * Print transformation if debug = on + */ + if (strcmp($inboundValue,$inboundArray[$inboundKey]) <> 0 && $this->debug) + ADOConnection::outp("{$this->databaseType} internal transformation: {$inboundArray[$inboundKey]} to {$inboundValue}"); + + if (strcmp($inboundValue,$inboundArray[$inboundKey]) <> 0) + /* + * Place the transformed value into the outbound array + */ + $outboundArray[$inboundKey] = $inboundValue; + } + + /* + * Any transformations are in the $outboundArray + */ + if ($inboundIsArray) + return $outboundArray; + + /* + * We passed a string in originally + */ + return $outboundArray[0]; + + } + +} + +class ADORecordset_mssql_n extends ADORecordset_mssql { + var $databaseType = "mssql_n"; + function __construct($id,$mode=false) + { + parent::__construct($id,$mode); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-mssqlnative.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-mssqlnative.inc.php new file mode 100644 index 000000000..23eb2c03d --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-mssqlnative.inc.php @@ -0,0 +1,1197 @@ += 0x4300) { +// docs say 4.2.0, but testing shows only since 4.3.0 does it work! + ini_set('mssql.datetimeconvert',0); +} else { + global $ADODB_mssql_mths; // array, months must be upper-case + $ADODB_mssql_date_order = 'mdy'; + $ADODB_mssql_mths = array( + 'JAN'=>1,'FEB'=>2,'MAR'=>3,'APR'=>4,'MAY'=>5,'JUN'=>6, + 'JUL'=>7,'AUG'=>8,'SEP'=>9,'OCT'=>10,'NOV'=>11,'DEC'=>12); +} + +class ADODB_mssqlnative extends ADOConnection { + var $databaseType = "mssqlnative"; + var $dataProvider = "mssqlnative"; + var $replaceQuote = "''"; // string to use to replace quotes + var $fmtDate = "'Y-m-d'"; + var $fmtTimeStamp = "'Y-m-d\TH:i:s'"; + var $hasInsertID = true; + var $substr = "substring"; + var $length = 'len'; + var $hasAffectedRows = true; + var $poorAffectedRows = false; + var $metaDatabasesSQL = "select name from sys.sysdatabases where name <> 'master'"; + var $metaTablesSQL="select name,case when type='U' then 'T' else 'V' end from sysobjects where (type='U' or type='V') and (name not in ('sysallocations','syscolumns','syscomments','sysdepends','sysfilegroups','sysfiles','sysfiles1','sysforeignkeys','sysfulltextcatalogs','sysindexes','sysindexkeys','sysmembers','sysobjects','syspermissions','sysprotects','sysreferences','systypes','sysusers','sysalternates','sysconstraints','syssegments','REFERENTIAL_CONSTRAINTS','CHECK_CONSTRAINTS','CONSTRAINT_TABLE_USAGE','CONSTRAINT_COLUMN_USAGE','VIEWS','VIEW_TABLE_USAGE','VIEW_COLUMN_USAGE','SCHEMATA','TABLES','TABLE_CONSTRAINTS','TABLE_PRIVILEGES','COLUMNS','COLUMN_DOMAIN_USAGE','COLUMN_PRIVILEGES','DOMAINS','DOMAIN_CONSTRAINTS','KEY_COLUMN_USAGE','dtproperties'))"; + var $metaColumnsSQL = + "select c.name, + t.name as type, + c.length, + c.xprec as precision, + c.xscale as scale, + c.isnullable as nullable, + c.cdefault as default_value, + c.xtype, + t.length as type_length, + sc.is_identity + from syscolumns c + join systypes t on t.xusertype=c.xusertype + join sysobjects o on o.id=c.id + join sys.tables st on st.name=o.name + join sys.columns sc on sc.object_id = st.object_id and sc.name=c.name + where o.name='%s'"; + var $hasTop = 'top'; // support mssql SELECT TOP 10 * FROM TABLE + var $hasGenID = true; + var $sysDate = 'convert(datetime,convert(char,GetDate(),102),102)'; + var $sysTimeStamp = 'GetDate()'; + var $maxParameterLen = 4000; + var $arrayClass = 'ADORecordSet_array_mssqlnative'; + var $uniqueSort = true; + var $leftOuter = '*='; + var $rightOuter = '=*'; + var $ansiOuter = true; // for mssql7 or later + var $identitySQL = 'select SCOPE_IDENTITY()'; // 'select SCOPE_IDENTITY'; # for mssql 2000 + var $uniqueOrderBy = true; + var $_bindInputArray = true; + var $_dropSeqSQL = "drop table %s"; + var $connectionInfo = array(); + var $cachedSchemaFlush = false; + var $sequences = false; + var $mssql_version = ''; + + function __construct() + { + if ($this->debug) { + ADOConnection::outp("
");
+			sqlsrv_set_error_handling( SQLSRV_ERRORS_LOG_ALL );
+			sqlsrv_log_set_severity( SQLSRV_LOG_SEVERITY_ALL );
+			sqlsrv_log_set_subsystems(SQLSRV_LOG_SYSTEM_ALL);
+			sqlsrv_configure('WarningsReturnAsErrors', 0);
+		} else {
+			sqlsrv_set_error_handling(0);
+			sqlsrv_log_set_severity(0);
+			sqlsrv_log_set_subsystems(SQLSRV_LOG_SYSTEM_ALL);
+			sqlsrv_configure('WarningsReturnAsErrors', 0);
+		}
+	}
+
+	/**
+	 * Initializes the SQL Server version.
+	 * Dies if connected to a non-supported version (2000 and older)
+	 */
+	function ServerVersion() {
+		$data = $this->ServerInfo();
+		preg_match('/^\d{2}/', $data['version'], $matches);
+		$version = (int)reset($matches);
+
+		// We only support SQL Server 2005 and up
+		if($version < 9) {
+			die("SQL SERVER VERSION {$data['version']} NOT SUPPORTED IN mssqlnative DRIVER");
+		}
+
+		$this->mssql_version = $version;
+	}
+
+	function ServerInfo() {
+    	global $ADODB_FETCH_MODE;
+		static $arr = false;
+		if (is_array($arr))
+			return $arr;
+		if ($this->fetchMode === false) {
+			$savem = $ADODB_FETCH_MODE;
+			$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
+		} elseif ($this->fetchMode >=0 && $this->fetchMode <=2) {
+			$savem = $this->fetchMode;
+		} else
+			$savem = $this->SetFetchMode(ADODB_FETCH_NUM);
+
+		$arrServerInfo = sqlsrv_server_info($this->_connectionID);
+		$ADODB_FETCH_MODE = $savem;
+		$arr['description'] = $arrServerInfo['SQLServerName'].' connected to '.$arrServerInfo['CurrentDatabase'];
+		$arr['version'] = $arrServerInfo['SQLServerVersion'];//ADOConnection::_findvers($arr['description']);
+		return $arr;
+	}
+
+	function IfNull( $field, $ifNull )
+	{
+		return " ISNULL($field, $ifNull) "; // if MS SQL Server
+	}
+
+	function _insertid()
+	{
+	// SCOPE_IDENTITY()
+	// Returns the last IDENTITY value inserted into an IDENTITY column in
+	// the same scope. A scope is a module -- a stored procedure, trigger,
+	// function, or batch. Thus, two statements are in the same scope if
+	// they are in the same stored procedure, function, or batch.
+		return $this->lastInsertID;
+	}
+
+	function _affectedrows()
+	{
+		if ($this->_queryID)
+		return sqlsrv_rows_affected($this->_queryID);
+	}
+
+	function GenID($seq='adodbseq',$start=1) {
+		if (!$this->mssql_version)
+			$this->ServerVersion();
+		switch($this->mssql_version){
+		case 9:
+		case 10:
+			return $this->GenID2008($seq, $start);
+			break;
+		default:
+			return $this->GenID2012($seq, $start);
+			break;
+		}
+	}
+
+	function CreateSequence($seq='adodbseq',$start=1)
+	{
+		if (!$this->mssql_version)
+			$this->ServerVersion();
+
+		switch($this->mssql_version){
+		case 9:
+		case 10:
+			return $this->CreateSequence2008($seq, $start);
+			break;
+		default:
+			return $this->CreateSequence2012($seq, $start);
+			break;
+		}
+
+	}
+
+	/**
+	 * For Server 2005,2008, duplicate a sequence with an identity table
+	 */
+	function CreateSequence2008($seq='adodbseq',$start=1)
+	{
+		if($this->debug) ADOConnection::outp("
CreateSequence($seq,$start)"); + sqlsrv_begin_transaction($this->_connectionID); + $start -= 1; + $this->Execute("create table $seq (id int)");//was float(53) + $ok = $this->Execute("insert into $seq with (tablock,holdlock) values($start)"); + if (!$ok) { + if($this->debug) ADOConnection::outp("
Error: ROLLBACK"); + sqlsrv_rollback($this->_connectionID); + return false; + } + sqlsrv_commit($this->_connectionID); + return true; + } + + /** + * Proper Sequences Only available to Server 2012 and up + */ + function CreateSequence2012($seq='adodbseq',$start=1){ + if (!$this->sequences){ + $sql = "SELECT name FROM sys.sequences"; + $this->sequences = $this->GetCol($sql); + } + $ok = $this->Execute("CREATE SEQUENCE $seq START WITH $start INCREMENT BY 1"); + if (!$ok) + die("CANNOT CREATE SEQUENCE" . print_r(sqlsrv_errors(),true)); + $this->sequences[] = $seq; + } + + /** + * For Server 2005,2008, duplicate a sequence with an identity table + */ + function GenID2008($seq='adodbseq',$start=1) + { + if($this->debug) ADOConnection::outp("
CreateSequence($seq,$start)"); + sqlsrv_begin_transaction($this->_connectionID); + $ok = $this->Execute("update $seq with (tablock,holdlock) set id = id + 1"); + if (!$ok) { + $start -= 1; + $this->Execute("create table $seq (id int)");//was float(53) + $ok = $this->Execute("insert into $seq with (tablock,holdlock) values($start)"); + if (!$ok) { + if($this->debug) ADOConnection::outp("
Error: ROLLBACK"); + sqlsrv_rollback($this->_connectionID); + return false; + } + } + $num = $this->GetOne("select id from $seq"); + sqlsrv_commit($this->_connectionID); + return $num; + } + /** + * Only available to Server 2012 and up + * Cannot do this the normal adodb way by trapping an error if the + * sequence does not exist because sql server will auto create a + * sequence with the starting number of -9223372036854775808 + */ + function GenID2012($seq='adodbseq',$start=1) + { + + /* + * First time in create an array of sequence names that we + * can use in later requests to see if the sequence exists + * the overhead is creating a list of sequences every time + * we need access to at least 1. If we really care about + * performance, we could maybe flag a 'nocheck' class variable + */ + if (!$this->sequences){ + $sql = "SELECT name FROM sys.sequences"; + $this->sequences = $this->GetCol($sql); + } + if (!is_array($this->sequences) + || is_array($this->sequences) && !in_array($seq,$this->sequences)){ + $this->CreateSequence2012($seq, $start); + + } + $num = $this->GetOne("SELECT NEXT VALUE FOR $seq"); + return $num; + } + + // Format date column in sql string given an input format that understands Y M D + function SQLDate($fmt, $col=false) + { + if (!$col) $col = $this->sysTimeStamp; + $s = ''; + + $len = strlen($fmt); + for ($i=0; $i < $len; $i++) { + if ($s) $s .= '+'; + $ch = $fmt[$i]; + switch($ch) { + case 'Y': + case 'y': + $s .= "datename(yyyy,$col)"; + break; + case 'M': + $s .= "convert(char(3),$col,0)"; + break; + case 'm': + $s .= "replace(str(month($col),2),' ','0')"; + break; + case 'Q': + case 'q': + $s .= "datename(quarter,$col)"; + break; + case 'D': + case 'd': + $s .= "replace(str(day($col),2),' ','0')"; + break; + case 'h': + $s .= "substring(convert(char(14),$col,0),13,2)"; + break; + + case 'H': + $s .= "replace(str(datepart(hh,$col),2),' ','0')"; + break; + + case 'i': + $s .= "replace(str(datepart(mi,$col),2),' ','0')"; + break; + case 's': + $s .= "replace(str(datepart(ss,$col),2),' ','0')"; + break; + case 'a': + case 'A': + $s .= "substring(convert(char(19),$col,0),18,2)"; + break; + + default: + if ($ch == '\\') { + $i++; + $ch = substr($fmt,$i,1); + } + $s .= $this->qstr($ch); + break; + } + } + return $s; + } + + + function BeginTrans() + { + if ($this->transOff) return true; + $this->transCnt += 1; + if ($this->debug) ADOConnection::outp('
begin transaction'); + sqlsrv_begin_transaction($this->_connectionID); + return true; + } + + function CommitTrans($ok=true) + { + if ($this->transOff) return true; + if ($this->debug) ADOConnection::outp('
commit transaction'); + if (!$ok) return $this->RollbackTrans(); + if ($this->transCnt) $this->transCnt -= 1; + sqlsrv_commit($this->_connectionID); + return true; + } + function RollbackTrans() + { + if ($this->transOff) return true; + if ($this->debug) ADOConnection::outp('
rollback transaction'); + if ($this->transCnt) $this->transCnt -= 1; + sqlsrv_rollback($this->_connectionID); + return true; + } + + function SetTransactionMode( $transaction_mode ) + { + $this->_transmode = $transaction_mode; + if (empty($transaction_mode)) { + $this->Execute('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); + return; + } + if (!stristr($transaction_mode,'isolation')) $transaction_mode = 'ISOLATION LEVEL '.$transaction_mode; + $this->Execute("SET TRANSACTION ".$transaction_mode); + } + + /* + Usage: + + $this->BeginTrans(); + $this->RowLock('table1,table2','table1.id=33 and table2.id=table1.id'); # lock row 33 for both tables + + # some operation on both tables table1 and table2 + + $this->CommitTrans(); + + See http://www.swynk.com/friends/achigrik/SQL70Locks.asp + */ + function RowLock($tables,$where,$col='1 as adodbignore') + { + if ($col == '1 as adodbignore') $col = 'top 1 null as ignore'; + if (!$this->transCnt) $this->BeginTrans(); + return $this->GetOne("select $col from $tables with (ROWLOCK,HOLDLOCK) where $where"); + } + + function SelectDB($dbName) + { + $this->database = $dbName; + $this->databaseName = $dbName; # obsolete, retained for compat with older adodb versions + if ($this->_connectionID) { + $rs = $this->Execute('USE '.$dbName); + if($rs) { + return true; + } else return false; + } + else return false; + } + + function ErrorMsg() + { + $retErrors = sqlsrv_errors(SQLSRV_ERR_ALL); + if($retErrors != null) { + foreach($retErrors as $arrError) { + $this->_errorMsg .= "SQLState: ".$arrError[ 'SQLSTATE']."\n"; + $this->_errorMsg .= "Error Code: ".$arrError[ 'code']."\n"; + $this->_errorMsg .= "Message: ".$arrError[ 'message']."\n"; + } + } else { + $this->_errorMsg = "No errors found"; + } + return $this->_errorMsg; + } + + function ErrorNo() + { + $err = sqlsrv_errors(SQLSRV_ERR_ALL); + if($err[0]) return $err[0]['code']; + else return 0; + } + + // returns true or false + function _connect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + if (!function_exists('sqlsrv_connect')) return null; + $connectionInfo = $this->connectionInfo; + $connectionInfo["Database"]=$argDatabasename; + $connectionInfo["UID"]=$argUsername; + $connectionInfo["PWD"]=$argPassword; + + foreach ($this->connectionParameters as $parameter=>$value) + $connectionInfo[$parameter] = $value; + + if ($this->debug) ADOConnection::outp("
connecting... hostname: $argHostname params: ".var_export($connectionInfo,true)); + //if ($this->debug) ADOConnection::outp("
_connectionID before: ".serialize($this->_connectionID)); + if(!($this->_connectionID = sqlsrv_connect($argHostname,$connectionInfo))) { + if ($this->debug) ADOConnection::outp( "
errors: ".print_r( sqlsrv_errors(), true)); + return false; + } + //if ($this->debug) ADOConnection::outp(" _connectionID after: ".serialize($this->_connectionID)); + //if ($this->debug) ADOConnection::outp("
defined functions:
".var_export(get_defined_functions(),true)."
"); + return true; + } + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + //return null;//not implemented. NOTE: Persistent connections have no effect if PHP is used as a CGI program. (FastCGI!) + return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabasename); + } + + function Prepare($sql) + { + return $sql; // prepare does not work properly with bind parameters as bind parameters are managed by sqlsrv_prepare! + + $stmt = sqlsrv_prepare( $this->_connectionID, $sql); + if (!$stmt) return $sql; + return array($sql,$stmt); + } + + // returns concatenated string + // MSSQL requires integers to be cast as strings + // automatically cast every datatype to VARCHAR(255) + // @author David Rogers (introspectshun) + function Concat() + { + $s = ""; + $arr = func_get_args(); + + // Split single record on commas, if possible + if (sizeof($arr) == 1) { + foreach ($arr as $arg) { + $args = explode(',', $arg); + } + $arr = $args; + } + + array_walk($arr, create_function('&$v', '$v = "CAST(" . $v . " AS VARCHAR(255))";')); + $s = implode('+',$arr); + if (sizeof($arr) > 0) return "$s"; + + return ''; + } + + /* + Unfortunately, it appears that mssql cannot handle varbinary > 255 chars + So all your blobs must be of type "image". + + Remember to set in php.ini the following... + + ; Valid range 0 - 2147483647. Default = 4096. + mssql.textlimit = 0 ; zero to pass through + + ; Valid range 0 - 2147483647. Default = 4096. + mssql.textsize = 0 ; zero to pass through + */ + function UpdateBlob($table,$column,$val,$where,$blobtype='BLOB') + { + + if (strtoupper($blobtype) == 'CLOB') { + $sql = "UPDATE $table SET $column='" . $val . "' WHERE $where"; + return $this->Execute($sql) != false; + } + $sql = "UPDATE $table SET $column=0x".bin2hex($val)." WHERE $where"; + return $this->Execute($sql) != false; + } + + // returns query ID if successful, otherwise false + function _query($sql,$inputarr=false) + { + $this->_errorMsg = false; + + if (is_array($sql)) $sql = $sql[1]; + + $insert = false; + // handle native driver flaw for retrieving the last insert ID + if(preg_match('/^\W*insert[\s\w()",.]+values\s*\((?:[^;\']|\'\'|(?:(?:\'\')*\'[^\']+\'(?:\'\')*))*;?$/i', $sql)) { + $insert = true; + $sql .= '; '.$this->identitySQL; // select scope_identity() + } + if($inputarr) { + $rez = sqlsrv_query($this->_connectionID, $sql, $inputarr); + } else { + $rez = sqlsrv_query($this->_connectionID,$sql); + } + + if ($this->debug) ADOConnection::outp("
running query: ".var_export($sql,true)."
input array: ".var_export($inputarr,true)."
result: ".var_export($rez,true)); + + if(!$rez) { + $rez = false; + } else if ($insert) { + // retrieve the last insert ID (where applicable) + while ( sqlsrv_next_result($rez) ) { + sqlsrv_fetch($rez); + $this->lastInsertID = sqlsrv_get_field($rez, 0); + } + } + return $rez; + } + + // returns true or false + function _close() + { + if ($this->transCnt) $this->RollbackTrans(); + $rez = @sqlsrv_close($this->_connectionID); + $this->_connectionID = false; + return $rez; + } + + // mssql uses a default date like Dec 30 2000 12:00AM + static function UnixDate($v) + { + return ADORecordSet_array_mssqlnative::UnixDate($v); + } + + static function UnixTimeStamp($v) + { + return ADORecordSet_array_mssqlnative::UnixTimeStamp($v); + } + + function MetaIndexes($table,$primary=false, $owner = false) + { + $table = $this->qstr($table); + + $sql = "SELECT i.name AS ind_name, C.name AS col_name, USER_NAME(O.uid) AS Owner, c.colid, k.Keyno, + CASE WHEN I.indid BETWEEN 1 AND 254 AND (I.status & 2048 = 2048 OR I.Status = 16402 AND O.XType = 'V') THEN 1 ELSE 0 END AS IsPK, + CASE WHEN I.status & 2 = 2 THEN 1 ELSE 0 END AS IsUnique + FROM dbo.sysobjects o INNER JOIN dbo.sysindexes I ON o.id = i.id + INNER JOIN dbo.sysindexkeys K ON I.id = K.id AND I.Indid = K.Indid + INNER JOIN dbo.syscolumns c ON K.id = C.id AND K.colid = C.Colid + WHERE LEFT(i.name, 8) <> '_WA_Sys_' AND o.status >= 0 AND O.Name LIKE $table + ORDER BY O.name, I.Name, K.keyno"; + + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + + $rs = $this->Execute($sql); + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + if (!is_object($rs)) { + return FALSE; + } + + $indexes = array(); + while ($row = $rs->FetchRow()) { + if (!$primary && $row[5]) continue; + + $indexes[$row[0]]['unique'] = $row[6]; + $indexes[$row[0]]['columns'][] = $row[1]; + } + return $indexes; + } + + function MetaForeignKeys($table, $owner=false, $upper=false) + { + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $table = $this->qstr(strtoupper($table)); + + $sql = + "select object_name(constid) as constraint_name, + col_name(fkeyid, fkey) as column_name, + object_name(rkeyid) as referenced_table_name, + col_name(rkeyid, rkey) as referenced_column_name + from sysforeignkeys + where upper(object_name(fkeyid)) = $table + order by constraint_name, referenced_table_name, keyno"; + + $constraints =& $this->GetArray($sql); + + $ADODB_FETCH_MODE = $save; + + $arr = false; + foreach($constraints as $constr) { + //print_r($constr); + $arr[$constr[0]][$constr[2]][] = $constr[1].'='.$constr[3]; + } + if (!$arr) return false; + + $arr2 = false; + + foreach($arr as $k => $v) { + foreach($v as $a => $b) { + if ($upper) $a = strtoupper($a); + $arr2[$a] = $b; + } + } + return $arr2; + } + + //From: Fernando Moreira + function MetaDatabases() + { + $this->SelectDB("master"); + $rs =& $this->Execute($this->metaDatabasesSQL); + $rows = $rs->GetRows(); + $ret = array(); + for($i=0;$iSelectDB($this->database); + if($ret) + return $ret; + else + return false; + } + + // "Stein-Aksel Basma" + // tested with MSSQL 2000 + function MetaPrimaryKeys($table, $owner=false) + { + global $ADODB_FETCH_MODE; + + $schema = ''; + $this->_findschema($table,$schema); + if (!$schema) $schema = $this->database; + if ($schema) $schema = "and k.table_catalog like '$schema%'"; + + $sql = "select distinct k.column_name,ordinal_position from information_schema.key_column_usage k, + information_schema.table_constraints tc + where tc.constraint_name = k.constraint_name and tc.constraint_type = + 'PRIMARY KEY' and k.table_name = '$table' $schema order by ordinal_position "; + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $a = $this->GetCol($sql); + $ADODB_FETCH_MODE = $savem; + + if ($a && sizeof($a)>0) return $a; + $false = false; + return $false; + } + + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + if ($mask) { + $save = $this->metaTablesSQL; + $mask = $this->qstr(($mask)); + $this->metaTablesSQL .= " AND name like $mask"; + } + $ret = ADOConnection::MetaTables($ttype,$showSchema); + + if ($mask) { + $this->metaTablesSQL = $save; + } + return $ret; + } + function MetaColumns($table, $upper=true, $schema=false){ + + # start adg + static $cached_columns = array(); + if ($this->cachedSchemaFlush) + $cached_columns = array(); + + if (array_key_exists($table,$cached_columns)){ + return $cached_columns[$table]; + } + # end adg + + if (!$this->mssql_version) + $this->ServerVersion(); + + $this->_findschema($table,$schema); + if ($schema) { + $dbName = $this->database; + $this->SelectDB($schema); + } + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + if ($this->fetchMode !== false) $savem = $this->SetFetchMode(false); + $rs = $this->Execute(sprintf($this->metaColumnsSQL,$table)); + + if ($schema) { + $this->SelectDB($dbName); + } + + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + if (!is_object($rs)) { + $false = false; + return $false; + } + + $retarr = array(); + while (!$rs->EOF){ + + $fld = new ADOFieldObject(); + if (array_key_exists(0,$rs->fields)) { + $fld->name = $rs->fields[0]; + $fld->type = $rs->fields[1]; + $fld->max_length = $rs->fields[2]; + $fld->precision = $rs->fields[3]; + $fld->scale = $rs->fields[4]; + $fld->not_null =!$rs->fields[5]; + $fld->has_default = $rs->fields[6]; + $fld->xtype = $rs->fields[7]; + $fld->type_length = $rs->fields[8]; + $fld->auto_increment= $rs->fields[9]; + } else { + $fld->name = $rs->fields['name']; + $fld->type = $rs->fields['type']; + $fld->max_length = $rs->fields['length']; + $fld->precision = $rs->fields['precision']; + $fld->scale = $rs->fields['scale']; + $fld->not_null =!$rs->fields['nullable']; + $fld->has_default = $rs->fields['default_value']; + $fld->xtype = $rs->fields['xtype']; + $fld->type_length = $rs->fields['type_length']; + $fld->auto_increment= $rs->fields['is_identity']; + } + + if ($save == ADODB_FETCH_NUM) + $retarr[] = $fld; + else + $retarr[strtoupper($fld->name)] = $fld; + + $rs->MoveNext(); + + } + $rs->Close(); + # start adg + $cached_columns[$table] = $retarr; + # end adg + return $retarr; + } + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordset_mssqlnative extends ADORecordSet { + + var $databaseType = "mssqlnative"; + var $canSeek = false; + var $fieldOffset = 0; + // _mths works only in non-localised system + + function __construct($id,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + + } + $this->fetchMode = $mode; + return parent::__construct($id,$mode); + } + + + function _initrs() + { + global $ADODB_COUNTRECS; + # KMN # if ($this->connection->debug) ADOConnection::outp("(before) ADODB_COUNTRECS: {$ADODB_COUNTRECS} _numOfRows: {$this->_numOfRows} _numOfFields: {$this->_numOfFields}"); + /*$retRowsAff = sqlsrv_rows_affected($this->_queryID);//"If you need to determine the number of rows a query will return before retrieving the actual results, appending a SELECT COUNT ... query would let you get that information, and then a call to next_result would move you to the "real" results." + ADOConnection::outp("rowsaff: ".serialize($retRowsAff)); + $this->_numOfRows = ($ADODB_COUNTRECS)? $retRowsAff:-1;*/ + $this->_numOfRows = -1;//not supported + $fieldmeta = sqlsrv_field_metadata($this->_queryID); + $this->_numOfFields = ($fieldmeta)? count($fieldmeta):-1; + # KMN # if ($this->connection->debug) ADOConnection::outp("(after) _numOfRows: {$this->_numOfRows} _numOfFields: {$this->_numOfFields}"); + /* + * Copy the oracle method and cache the metadata at init time + */ + if ($this->_numOfFields>0) { + $this->_fieldobjs = array(); + $max = $this->_numOfFields; + for ($i=0;$i<$max; $i++) $this->_fieldobjs[] = $this->_FetchField($i); + } + + } + + + //Contributed by "Sven Axelsson" + // get next resultset - requires PHP 4.0.5 or later + function NextRecordSet() + { + if (!sqlsrv_next_result($this->_queryID)) return false; + $this->_inited = false; + $this->bind = false; + $this->_currentRow = -1; + $this->Init(); + return true; + } + + /* Use associative array to get fields array */ + function Fields($colname) + { + if ($this->fetchMode != ADODB_FETCH_NUM) return $this->fields[$colname]; + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + /* Returns: an object containing field information. + Get column information in the Recordset object. fetchField() can be used in order to obtain information about + fields in a certain query result. If the field offset isn't specified, the next field that wasn't yet retrieved by + fetchField() is retrieved. + Designed By jcortinap#jc.com.mx + */ + function _FetchField($fieldOffset = -1) + { + $_typeConversion = array( + -155 => 'datetimeoffset', + -154 => 'time', + -152 => 'xml', + -151 => 'udt', + -11 => 'uniqueidentifier', + -10 => 'ntext', + -9 => 'nvarchar', + -8 => 'nchar', + -7 => 'bit', + -6 => 'tinyint', + -5 => 'bigint', + -4 => 'image', + -3 => 'varbinary', + -2 => 'timestamp', + -1 => 'text', + 1 => 'char', + 2 => 'numeric', + 3 => 'decimal', + 4 => 'int', + 5 => 'smallint', + 6 => 'float', + 7 => 'real', + 12 => 'varchar', + 91 => 'date', + 93 => 'datetime' + ); + + $fa = @sqlsrv_field_metadata($this->_queryID); + if ($fieldOffset != -1) { + $fa = $fa[$fieldOffset]; + } + $false = false; + if (empty($fa)) { + $f = false;//PHP Notice: Only variable references should be returned by reference + } + else + { + // Convert to an object + $fa = array_change_key_case($fa, CASE_LOWER); + $fb = array(); + if ($fieldOffset != -1) + { + $fb = array( + 'name' => $fa['name'], + 'max_length' => $fa['size'], + 'column_source' => $fa['name'], + 'type' => $_typeConversion[$fa['type']] + ); + } + else + { + foreach ($fa as $key => $value) + { + $fb[] = array( + 'name' => $value['name'], + 'max_length' => $value['size'], + 'column_source' => $value['name'], + 'type' => $_typeConversion[$value['type']] + ); + } + } + $f = (object) $fb; + } + return $f; + } + + /* + * Fetchfield copies the oracle method, it loads the field information + * into the _fieldobjs array once, to save multiple calls to the + * sqlsrv_field_metadata function + * + * @author KM Newnham + * @date 02/20/2013 + */ + function FetchField($fieldOffset = -1) + { + return $this->_fieldobjs[$fieldOffset]; + } + + function _seek($row) + { + return false;//There is no support for cursors in the driver at this time. All data is returned via forward-only streams. + } + + // speedup + function MoveNext() + { + //# KMN # if ($this->connection->debug) ADOConnection::outp("movenext()"); + //# KMN # if ($this->connection->debug) ADOConnection::outp("eof (beginning): ".$this->EOF); + if ($this->EOF) return false; + + $this->_currentRow++; + // # KMN # if ($this->connection->debug) ADOConnection::outp("_currentRow: ".$this->_currentRow); + + if ($this->_fetch()) return true; + $this->EOF = true; + //# KMN # if ($this->connection->debug) ADOConnection::outp("eof (end): ".$this->EOF); + + return false; + } + + + // INSERT UPDATE DELETE returns false even if no error occurs in 4.0.4 + // also the date format has been changed from YYYY-mm-dd to dd MMM YYYY in 4.0.4. Idiot! + function _fetch($ignore_fields=false) + { + # KMN # if ($this->connection->debug) ADOConnection::outp("_fetch()"); + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + if ($this->fetchMode & ADODB_FETCH_NUM) { + //# KMN # if ($this->connection->debug) ADOConnection::outp("fetch mode: both"); + $this->fields = @sqlsrv_fetch_array($this->_queryID,SQLSRV_FETCH_BOTH); + } else { + //# KMN # if ($this->connection->debug) ADOConnection::outp("fetch mode: assoc"); + $this->fields = @sqlsrv_fetch_array($this->_queryID,SQLSRV_FETCH_ASSOC); + } + + if (is_array($this->fields)) { + if (ADODB_ASSOC_CASE == 0) { + foreach($this->fields as $k=>$v) { + $this->fields[strtolower($k)] = $v; + } + } else if (ADODB_ASSOC_CASE == 1) { + foreach($this->fields as $k=>$v) { + $this->fields[strtoupper($k)] = $v; + } + } + } + } else { + //# KMN # if ($this->connection->debug) ADOConnection::outp("fetch mode: num"); + $this->fields = @sqlsrv_fetch_array($this->_queryID,SQLSRV_FETCH_NUMERIC); + } + if(is_array($this->fields) && array_key_exists(1,$this->fields) && !array_key_exists(0,$this->fields)) {//fix fetch numeric keys since they're not 0 based + $arrFixed = array(); + foreach($this->fields as $key=>$value) { + if(is_numeric($key)) { + $arrFixed[$key-1] = $value; + } else { + $arrFixed[$key] = $value; + } + } + //if($this->connection->debug) ADOConnection::outp("
fixing non 0 based return array, old: ".print_r($this->fields,true)." new: ".print_r($arrFixed,true)); + $this->fields = $arrFixed; + } + if(is_array($this->fields)) { + foreach($this->fields as $key=>$value) { + if (is_object($value) && method_exists($value, 'format')) {//is DateTime object + $this->fields[$key] = $value->format("Y-m-d\TH:i:s\Z"); + } + } + } + if($this->fields === null) $this->fields = false; + # KMN # if ($this->connection->debug) ADOConnection::outp("
after _fetch, fields:
".print_r($this->fields,true)." backtrace: ".adodb_backtrace(false));
+		return $this->fields;
+	}
+
+	/*	close() only needs to be called if you are worried about using too much memory while your script
+		is running. All associated result memory for the specified result identifier will automatically be freed.	*/
+	function _close()
+	{
+		if(is_object($this->_queryID)) {
+			$rez = sqlsrv_free_stmt($this->_queryID);
+			$this->_queryID = false;
+			return $rez;
+		}
+		return true;
+	}
+
+	// mssql uses a default date like Dec 30 2000 12:00AM
+	static function UnixDate($v)
+	{
+		return ADORecordSet_array_mssqlnative::UnixDate($v);
+	}
+
+	static function UnixTimeStamp($v)
+	{
+		return ADORecordSet_array_mssqlnative::UnixTimeStamp($v);
+	}
+}
+
+
+class ADORecordSet_array_mssqlnative extends ADORecordSet_array {
+	function __construct($id=-1,$mode=false)
+	{
+		parent::__construct($id,$mode);
+	}
+
+		// mssql uses a default date like Dec 30 2000 12:00AM
+	static function UnixDate($v)
+	{
+
+		if (is_numeric(substr($v,0,1)) && ADODB_PHPVER >= 0x4200) return parent::UnixDate($v);
+
+		global $ADODB_mssql_mths,$ADODB_mssql_date_order;
+
+		//Dec 30 2000 12:00AM
+		if ($ADODB_mssql_date_order == 'dmy') {
+			if (!preg_match( "|^([0-9]{1,2})[-/\. ]+([A-Za-z]{3})[-/\. ]+([0-9]{4})|" ,$v, $rr)) {
+				return parent::UnixDate($v);
+			}
+			if ($rr[3] <= TIMESTAMP_FIRST_YEAR) return 0;
+
+			$theday = $rr[1];
+			$themth =  substr(strtoupper($rr[2]),0,3);
+		} else {
+			if (!preg_match( "|^([A-Za-z]{3})[-/\. ]+([0-9]{1,2})[-/\. ]+([0-9]{4})|" ,$v, $rr)) {
+				return parent::UnixDate($v);
+			}
+			if ($rr[3] <= TIMESTAMP_FIRST_YEAR) return 0;
+
+			$theday = $rr[2];
+			$themth = substr(strtoupper($rr[1]),0,3);
+		}
+		$themth = $ADODB_mssql_mths[$themth];
+		if ($themth <= 0) return false;
+		// h-m-s-MM-DD-YY
+		return  adodb_mktime(0,0,0,$themth,$theday,$rr[3]);
+	}
+
+	static function UnixTimeStamp($v)
+	{
+
+		if (is_numeric(substr($v,0,1)) && ADODB_PHPVER >= 0x4200) return parent::UnixTimeStamp($v);
+
+		global $ADODB_mssql_mths,$ADODB_mssql_date_order;
+
+		//Dec 30 2000 12:00AM
+		 if ($ADODB_mssql_date_order == 'dmy') {
+			 if (!preg_match( "|^([0-9]{1,2})[-/\. ]+([A-Za-z]{3})[-/\. ]+([0-9]{4}) +([0-9]{1,2}):([0-9]{1,2}) *([apAP]{0,1})|"
+			,$v, $rr)) return parent::UnixTimeStamp($v);
+			if ($rr[3] <= TIMESTAMP_FIRST_YEAR) return 0;
+
+			$theday = $rr[1];
+			$themth =  substr(strtoupper($rr[2]),0,3);
+		} else {
+			if (!preg_match( "|^([A-Za-z]{3})[-/\. ]+([0-9]{1,2})[-/\. ]+([0-9]{4}) +([0-9]{1,2}):([0-9]{1,2}) *([apAP]{0,1})|"
+			,$v, $rr)) return parent::UnixTimeStamp($v);
+			if ($rr[3] <= TIMESTAMP_FIRST_YEAR) return 0;
+
+			$theday = $rr[2];
+			$themth = substr(strtoupper($rr[1]),0,3);
+		}
+
+		$themth = $ADODB_mssql_mths[$themth];
+		if ($themth <= 0) return false;
+
+		switch (strtoupper($rr[6])) {
+		case 'P':
+			if ($rr[4]<12) $rr[4] += 12;
+			break;
+		case 'A':
+			if ($rr[4]==12) $rr[4] = 0;
+			break;
+		default:
+			break;
+		}
+		// h-m-s-MM-DD-YY
+		return  adodb_mktime($rr[4],$rr[5],0,$themth,$theday,$rr[3]);
+	}
+}
+
+/*
+Code Example 1:
+
+select	object_name(constid) as constraint_name,
+		object_name(fkeyid) as table_name,
+		col_name(fkeyid, fkey) as column_name,
+	object_name(rkeyid) as referenced_table_name,
+	col_name(rkeyid, rkey) as referenced_column_name
+from sysforeignkeys
+where object_name(fkeyid) = x
+order by constraint_name, table_name, referenced_table_name,  keyno
+
+Code Example 2:
+select	constraint_name,
+	column_name,
+	ordinal_position
+from information_schema.key_column_usage
+where constraint_catalog = db_name()
+and table_name = x
+order by constraint_name, ordinal_position
+
+http://www.databasejournal.com/scripts/article.php/1440551
+*/
diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-mssqlpo.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-mssqlpo.inc.php
new file mode 100644
index 000000000..cd6a2850a
--- /dev/null
+++ b/app/vendor/adodb/adodb-php/drivers/adodb-mssqlpo.inc.php
@@ -0,0 +1,58 @@
+_has_mssql_init) {
+			ADOConnection::outp( "PrepareSP: mssql_init only available since PHP 4.1.0");
+			return $sql;
+		}
+		if (is_string($sql)) $sql = str_replace('||','+',$sql);
+		$stmt = mssql_init($sql,$this->_connectionID);
+		if (!$stmt)  return $sql;
+		return array($sql,$stmt);
+	}
+
+	function _query($sql,$inputarr=false)
+	{
+		if (is_string($sql)) $sql = str_replace('||','+',$sql);
+		return ADODB_mssql::_query($sql,$inputarr);
+	}
+}
+
+class ADORecordset_mssqlpo extends ADORecordset_mssql {
+	var $databaseType = "mssqlpo";
+	function __construct($id,$mode=false)
+	{
+		parent::__construct($id,$mode);
+	}
+}
diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-mysql.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-mysql.inc.php
new file mode 100644
index 000000000..2d999c6bc
--- /dev/null
+++ b/app/vendor/adodb/adodb-php/drivers/adodb-mysql.inc.php
@@ -0,0 +1,891 @@
+rsPrefix .= 'ext_';
+	}
+
+
+	// SetCharSet - switch the client encoding
+	function SetCharSet($charset_name)
+	{
+		if (!function_exists('mysql_set_charset')) {
+			return false;
+		}
+
+		if ($this->charSet !== $charset_name) {
+			$ok = @mysql_set_charset($charset_name,$this->_connectionID);
+			if ($ok) {
+				$this->charSet = $charset_name;
+				return true;
+			}
+			return false;
+		}
+		return true;
+	}
+
+	function ServerInfo()
+	{
+		$arr['description'] = ADOConnection::GetOne("select version()");
+		$arr['version'] = ADOConnection::_findvers($arr['description']);
+		return $arr;
+	}
+
+	function IfNull( $field, $ifNull )
+	{
+		return " IFNULL($field, $ifNull) "; // if MySQL
+	}
+
+	function MetaProcedures($NamePattern = false, $catalog = null, $schemaPattern = null)
+	{
+		// save old fetch mode
+		global $ADODB_FETCH_MODE;
+
+		$false = false;
+		$save = $ADODB_FETCH_MODE;
+		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
+
+		if ($this->fetchMode !== FALSE) {
+			$savem = $this->SetFetchMode(FALSE);
+		}
+
+		$procedures = array ();
+
+		// get index details
+
+		$likepattern = '';
+		if ($NamePattern) {
+			$likepattern = " LIKE '".$NamePattern."'";
+		}
+		$rs = $this->Execute('SHOW PROCEDURE STATUS'.$likepattern);
+		if (is_object($rs)) {
+
+			// parse index data into array
+			while ($row = $rs->FetchRow()) {
+				$procedures[$row[1]] = array(
+					'type' => 'PROCEDURE',
+					'catalog' => '',
+					'schema' => '',
+					'remarks' => $row[7],
+				);
+			}
+		}
+
+		$rs = $this->Execute('SHOW FUNCTION STATUS'.$likepattern);
+		if (is_object($rs)) {
+			// parse index data into array
+			while ($row = $rs->FetchRow()) {
+				$procedures[$row[1]] = array(
+					'type' => 'FUNCTION',
+					'catalog' => '',
+					'schema' => '',
+					'remarks' => $row[7]
+				);
+			}
+		}
+
+		// restore fetchmode
+		if (isset($savem)) {
+			$this->SetFetchMode($savem);
+		}
+		$ADODB_FETCH_MODE = $save;
+
+		return $procedures;
+	}
+
+	/**
+	 * Retrieves a list of tables based on given criteria
+	 *
+	 * @param string $ttype Table type = 'TABLE', 'VIEW' or false=both (default)
+	 * @param string $showSchema schema name, false = current schema (default)
+	 * @param string $mask filters the table by name
+	 *
+	 * @return array list of tables
+	 */
+	function MetaTables($ttype=false,$showSchema=false,$mask=false)
+	{
+		$save = $this->metaTablesSQL;
+		if ($showSchema && is_string($showSchema)) {
+			$this->metaTablesSQL .= $this->qstr($showSchema);
+		} else {
+			$this->metaTablesSQL .= "schema()";
+		}
+
+		if ($mask) {
+			$mask = $this->qstr($mask);
+			$this->metaTablesSQL .= " AND table_name LIKE $mask";
+		}
+		$ret = ADOConnection::MetaTables($ttype,$showSchema);
+
+		$this->metaTablesSQL = $save;
+		return $ret;
+	}
+
+
+	function MetaIndexes ($table, $primary = FALSE, $owner=false)
+	{
+		// save old fetch mode
+		global $ADODB_FETCH_MODE;
+
+		$false = false;
+		$save = $ADODB_FETCH_MODE;
+		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
+		if ($this->fetchMode !== FALSE) {
+			$savem = $this->SetFetchMode(FALSE);
+		}
+
+		// get index details
+		$rs = $this->Execute(sprintf('SHOW INDEX FROM %s',$table));
+
+		// restore fetchmode
+		if (isset($savem)) {
+			$this->SetFetchMode($savem);
+		}
+		$ADODB_FETCH_MODE = $save;
+
+		if (!is_object($rs)) {
+			return $false;
+		}
+
+		$indexes = array ();
+
+		// parse index data into array
+		while ($row = $rs->FetchRow()) {
+			if ($primary == FALSE AND $row[2] == 'PRIMARY') {
+				continue;
+			}
+
+			if (!isset($indexes[$row[2]])) {
+				$indexes[$row[2]] = array(
+					'unique' => ($row[1] == 0),
+					'columns' => array()
+				);
+			}
+
+			$indexes[$row[2]]['columns'][$row[3] - 1] = $row[4];
+		}
+
+		// sort columns by order in the index
+		foreach ( array_keys ($indexes) as $index )
+		{
+			ksort ($indexes[$index]['columns']);
+		}
+
+		return $indexes;
+	}
+
+
+	// if magic quotes disabled, use mysql_real_escape_string()
+	function qstr($s,$magic_quotes=false)
+	{
+		if (is_null($s)) return 'NULL';
+		if (!$magic_quotes) {
+
+			if (ADODB_PHPVER >= 0x4300) {
+				if (is_resource($this->_connectionID))
+					return "'".mysql_real_escape_string($s,$this->_connectionID)."'";
+			}
+			if ($this->replaceQuote[0] == '\\'){
+				$s = adodb_str_replace(array('\\',"\0"),array('\\\\',"\\\0"),$s);
+			}
+			return "'".str_replace("'",$this->replaceQuote,$s)."'";
+		}
+
+		// undo magic quotes for "
+		$s = str_replace('\\"','"',$s);
+		return "'$s'";
+	}
+
+	function _insertid()
+	{
+		return ADOConnection::GetOne('SELECT LAST_INSERT_ID()');
+		//return mysql_insert_id($this->_connectionID);
+	}
+
+	function GetOne($sql,$inputarr=false)
+	{
+	global $ADODB_GETONE_EOF;
+		if ($this->compat323 == false && strncasecmp($sql,'sele',4) == 0) {
+			$rs = $this->SelectLimit($sql,1,-1,$inputarr);
+			if ($rs) {
+				$rs->Close();
+				if ($rs->EOF) return $ADODB_GETONE_EOF;
+				return reset($rs->fields);
+			}
+		} else {
+			return ADOConnection::GetOne($sql,$inputarr);
+		}
+		return false;
+	}
+
+	function BeginTrans()
+	{
+		if ($this->debug) ADOConnection::outp("Transactions not supported in 'mysql' driver. Use 'mysqlt' or 'mysqli' driver");
+	}
+
+	function _affectedrows()
+	{
+			return mysql_affected_rows($this->_connectionID);
+	}
+
+	 // See http://www.mysql.com/doc/M/i/Miscellaneous_functions.html
+	// Reference on Last_Insert_ID on the recommended way to simulate sequences
+	var $_genIDSQL = "update %s set id=LAST_INSERT_ID(id+1);";
+	var $_genSeqSQL = "create table if not exists %s (id int not null)";
+	var $_genSeqCountSQL = "select count(*) from %s";
+	var $_genSeq2SQL = "insert into %s values (%s)";
+	var $_dropSeqSQL = "drop table if exists %s";
+
+	function CreateSequence($seqname='adodbseq',$startID=1)
+	{
+		if (empty($this->_genSeqSQL)) return false;
+		$u = strtoupper($seqname);
+
+		$ok = $this->Execute(sprintf($this->_genSeqSQL,$seqname));
+		if (!$ok) return false;
+		return $this->Execute(sprintf($this->_genSeq2SQL,$seqname,$startID-1));
+	}
+
+
+	function GenID($seqname='adodbseq',$startID=1)
+	{
+		// post-nuke sets hasGenID to false
+		if (!$this->hasGenID) return false;
+
+		$savelog = $this->_logsql;
+		$this->_logsql = false;
+		$getnext = sprintf($this->_genIDSQL,$seqname);
+		$holdtransOK = $this->_transOK; // save the current status
+		$rs = @$this->Execute($getnext);
+		if (!$rs) {
+			if ($holdtransOK) $this->_transOK = true; //if the status was ok before reset
+			$u = strtoupper($seqname);
+			$this->Execute(sprintf($this->_genSeqSQL,$seqname));
+			$cnt = $this->GetOne(sprintf($this->_genSeqCountSQL,$seqname));
+			if (!$cnt) $this->Execute(sprintf($this->_genSeq2SQL,$seqname,$startID-1));
+			$rs = $this->Execute($getnext);
+		}
+
+		if ($rs) {
+			$this->genID = mysql_insert_id($this->_connectionID);
+			$rs->Close();
+		} else
+			$this->genID = 0;
+
+		$this->_logsql = $savelog;
+		return $this->genID;
+	}
+
+	function MetaDatabases()
+	{
+		$qid = mysql_list_dbs($this->_connectionID);
+		$arr = array();
+		$i = 0;
+		$max = mysql_num_rows($qid);
+		while ($i < $max) {
+			$db = mysql_tablename($qid,$i);
+			if ($db != 'mysql') $arr[] = $db;
+			$i += 1;
+		}
+		return $arr;
+	}
+
+
+	// Format date column in sql string given an input format that understands Y M D
+	function SQLDate($fmt, $col=false)
+	{
+		if (!$col) $col = $this->sysTimeStamp;
+		$s = 'DATE_FORMAT('.$col.",'";
+		$concat = false;
+		$len = strlen($fmt);
+		for ($i=0; $i < $len; $i++) {
+			$ch = $fmt[$i];
+			switch($ch) {
+
+			default:
+				if ($ch == '\\') {
+					$i++;
+					$ch = substr($fmt,$i,1);
+				}
+				/** FALL THROUGH */
+			case '-':
+			case '/':
+				$s .= $ch;
+				break;
+
+			case 'Y':
+			case 'y':
+				$s .= '%Y';
+				break;
+			case 'M':
+				$s .= '%b';
+				break;
+
+			case 'm':
+				$s .= '%m';
+				break;
+			case 'D':
+			case 'd':
+				$s .= '%d';
+				break;
+
+			case 'Q':
+			case 'q':
+				$s .= "'),Quarter($col)";
+
+				if ($len > $i+1) $s .= ",DATE_FORMAT($col,'";
+				else $s .= ",('";
+				$concat = true;
+				break;
+
+			case 'H':
+				$s .= '%H';
+				break;
+
+			case 'h':
+				$s .= '%I';
+				break;
+
+			case 'i':
+				$s .= '%i';
+				break;
+
+			case 's':
+				$s .= '%s';
+				break;
+
+			case 'a':
+			case 'A':
+				$s .= '%p';
+				break;
+
+			case 'w':
+				$s .= '%w';
+				break;
+
+			 case 'W':
+				$s .= '%U';
+				break;
+
+			case 'l':
+				$s .= '%W';
+				break;
+			}
+		}
+		$s.="')";
+		if ($concat) $s = "CONCAT($s)";
+		return $s;
+	}
+
+
+	// returns concatenated string
+	// much easier to run "mysqld --ansi" or "mysqld --sql-mode=PIPES_AS_CONCAT" and use || operator
+	function Concat()
+	{
+		$s = "";
+		$arr = func_get_args();
+
+		// suggestion by andrew005@mnogo.ru
+		$s = implode(',',$arr);
+		if (strlen($s) > 0) return "CONCAT($s)";
+		else return '';
+	}
+
+	function OffsetDate($dayFraction,$date=false)
+	{
+		if (!$date) $date = $this->sysDate;
+
+		$fraction = $dayFraction * 24 * 3600;
+		return '('. $date . ' + INTERVAL ' .	 $fraction.' SECOND)';
+
+//		return "from_unixtime(unix_timestamp($date)+$fraction)";
+	}
+
+	// returns true or false
+	function _connect($argHostname, $argUsername, $argPassword, $argDatabasename)
+	{
+		if (!empty($this->port)) $argHostname .= ":".$this->port;
+
+		if (ADODB_PHPVER >= 0x4300)
+			$this->_connectionID = mysql_connect($argHostname,$argUsername,$argPassword,
+												$this->forceNewConnect,$this->clientFlags);
+		else if (ADODB_PHPVER >= 0x4200)
+			$this->_connectionID = mysql_connect($argHostname,$argUsername,$argPassword,
+												$this->forceNewConnect);
+		else
+			$this->_connectionID = mysql_connect($argHostname,$argUsername,$argPassword);
+
+		if ($this->_connectionID === false) return false;
+		if ($argDatabasename) return $this->SelectDB($argDatabasename);
+		return true;
+	}
+
+	// returns true or false
+	function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
+	{
+		if (!empty($this->port)) $argHostname .= ":".$this->port;
+
+		if (ADODB_PHPVER >= 0x4300)
+			$this->_connectionID = mysql_pconnect($argHostname,$argUsername,$argPassword,$this->clientFlags);
+		else
+			$this->_connectionID = mysql_pconnect($argHostname,$argUsername,$argPassword);
+		if ($this->_connectionID === false) return false;
+		if ($this->autoRollback) $this->RollbackTrans();
+		if ($argDatabasename) return $this->SelectDB($argDatabasename);
+		return true;
+	}
+
+	function _nconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
+	{
+		$this->forceNewConnect = true;
+		return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabasename);
+	}
+
+	function MetaColumns($table, $normalize=true)
+	{
+		$this->_findschema($table,$schema);
+		if ($schema) {
+			$dbName = $this->database;
+			$this->SelectDB($schema);
+		}
+		global $ADODB_FETCH_MODE;
+		$save = $ADODB_FETCH_MODE;
+		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
+
+		if ($this->fetchMode !== false) $savem = $this->SetFetchMode(false);
+		$rs = $this->Execute(sprintf($this->metaColumnsSQL,$table));
+
+		if ($schema) {
+			$this->SelectDB($dbName);
+		}
+
+		if (isset($savem)) $this->SetFetchMode($savem);
+		$ADODB_FETCH_MODE = $save;
+		if (!is_object($rs)) {
+			$false = false;
+			return $false;
+		}
+
+		$retarr = array();
+		while (!$rs->EOF){
+			$fld = new ADOFieldObject();
+			$fld->name = $rs->fields[0];
+			$type = $rs->fields[1];
+
+			// split type into type(length):
+			$fld->scale = null;
+			if (preg_match("/^(.+)\((\d+),(\d+)/", $type, $query_array)) {
+				$fld->type = $query_array[1];
+				$fld->max_length = is_numeric($query_array[2]) ? $query_array[2] : -1;
+				$fld->scale = is_numeric($query_array[3]) ? $query_array[3] : -1;
+			} elseif (preg_match("/^(.+)\((\d+)/", $type, $query_array)) {
+				$fld->type = $query_array[1];
+				$fld->max_length = is_numeric($query_array[2]) ? $query_array[2] : -1;
+			} elseif (preg_match("/^(enum)\((.*)\)$/i", $type, $query_array)) {
+				$fld->type = $query_array[1];
+				$arr = explode(",",$query_array[2]);
+				$fld->enums = $arr;
+				$zlen = max(array_map("strlen",$arr)) - 2; // PHP >= 4.0.6
+				$fld->max_length = ($zlen > 0) ? $zlen : 1;
+			} else {
+				$fld->type = $type;
+				$fld->max_length = -1;
+			}
+			$fld->not_null = ($rs->fields[2] != 'YES');
+			$fld->primary_key = ($rs->fields[3] == 'PRI');
+			$fld->auto_increment = (strpos($rs->fields[5], 'auto_increment') !== false);
+			$fld->binary = (strpos($type,'blob') !== false || strpos($type,'binary') !== false);
+			$fld->unsigned = (strpos($type,'unsigned') !== false);
+			$fld->zerofill = (strpos($type,'zerofill') !== false);
+
+			if (!$fld->binary) {
+				$d = $rs->fields[4];
+				if ($d != '' && $d != 'NULL') {
+					$fld->has_default = true;
+					$fld->default_value = $d;
+				} else {
+					$fld->has_default = false;
+				}
+			}
+
+			if ($save == ADODB_FETCH_NUM) {
+				$retarr[] = $fld;
+			} else {
+				$retarr[strtoupper($fld->name)] = $fld;
+			}
+				$rs->MoveNext();
+			}
+
+			$rs->Close();
+			return $retarr;
+	}
+
+	// returns true or false
+	function SelectDB($dbName)
+	{
+		$this->database = $dbName;
+		$this->databaseName = $dbName; # obsolete, retained for compat with older adodb versions
+		if ($this->_connectionID) {
+			return @mysql_select_db($dbName,$this->_connectionID);
+		}
+		else return false;
+	}
+
+	// parameters use PostgreSQL convention, not MySQL
+	function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false,$secs=0)
+	{
+		$offsetStr =($offset>=0) ? ((integer)$offset)."," : '';
+		// jason judge, see http://phplens.com/lens/lensforum/msgs.php?id=9220
+		if ($nrows < 0) $nrows = '18446744073709551615';
+
+		if ($secs)
+			$rs = $this->CacheExecute($secs,$sql." LIMIT $offsetStr".((integer)$nrows),$inputarr);
+		else
+			$rs = $this->Execute($sql." LIMIT $offsetStr".((integer)$nrows),$inputarr);
+		return $rs;
+	}
+
+	// returns queryID or false
+	function _query($sql,$inputarr=false)
+	{
+
+	return mysql_query($sql,$this->_connectionID);
+	/*
+	global $ADODB_COUNTRECS;
+		if($ADODB_COUNTRECS)
+			return mysql_query($sql,$this->_connectionID);
+		else
+			return @mysql_unbuffered_query($sql,$this->_connectionID); // requires PHP >= 4.0.6
+	*/
+	}
+
+	/*	Returns: the last error message from previous database operation	*/
+	function ErrorMsg()
+	{
+
+		if ($this->_logsql) return $this->_errorMsg;
+		if (empty($this->_connectionID)) $this->_errorMsg = @mysql_error();
+		else $this->_errorMsg = @mysql_error($this->_connectionID);
+		return $this->_errorMsg;
+	}
+
+	/*	Returns: the last error number from previous database operation	*/
+	function ErrorNo()
+	{
+		if ($this->_logsql) return $this->_errorCode;
+		if (empty($this->_connectionID)) return @mysql_errno();
+		else return @mysql_errno($this->_connectionID);
+	}
+
+	// returns true or false
+	function _close()
+	{
+		@mysql_close($this->_connectionID);
+
+		$this->charSet = '';
+		$this->_connectionID = false;
+	}
+
+
+	/*
+	* Maximum size of C field
+	*/
+	function CharMax()
+	{
+		return 255;
+	}
+
+	/*
+	* Maximum size of X field
+	*/
+	function TextMax()
+	{
+		return 4294967295;
+	}
+
+	// "Innox - Juan Carlos Gonzalez" 
+	function MetaForeignKeys( $table, $owner = FALSE, $upper = FALSE, $associative = FALSE )
+	{
+	 global $ADODB_FETCH_MODE;
+		if ($ADODB_FETCH_MODE == ADODB_FETCH_ASSOC || $this->fetchMode == ADODB_FETCH_ASSOC) $associative = true;
+
+		if ( !empty($owner) ) {
+			$table = "$owner.$table";
+		}
+		$a_create_table = $this->getRow(sprintf('SHOW CREATE TABLE %s', $table));
+		if ($associative) {
+			$create_sql = isset($a_create_table["Create Table"]) ? $a_create_table["Create Table"] : $a_create_table["Create View"];
+		} else {
+			$create_sql = $a_create_table[1];
+		}
+
+		$matches = array();
+
+		if (!preg_match_all("/FOREIGN KEY \(`(.*?)`\) REFERENCES `(.*?)` \(`(.*?)`\)/", $create_sql, $matches)) return false;
+		$foreign_keys = array();
+		$num_keys = count($matches[0]);
+		for ( $i = 0; $i < $num_keys; $i ++ ) {
+			$my_field  = explode('`, `', $matches[1][$i]);
+			$ref_table = $matches[2][$i];
+			$ref_field = explode('`, `', $matches[3][$i]);
+
+			if ( $upper ) {
+				$ref_table = strtoupper($ref_table);
+			}
+
+			// see https://sourceforge.net/tracker/index.php?func=detail&aid=2287278&group_id=42718&atid=433976
+			if (!isset($foreign_keys[$ref_table])) {
+				$foreign_keys[$ref_table] = array();
+			}
+			$num_fields = count($my_field);
+			for ( $j = 0; $j < $num_fields; $j ++ ) {
+				if ( $associative ) {
+					$foreign_keys[$ref_table][$ref_field[$j]] = $my_field[$j];
+				} else {
+					$foreign_keys[$ref_table][] = "{$my_field[$j]}={$ref_field[$j]}";
+				}
+			}
+		}
+
+		return $foreign_keys;
+	}
+
+
+}
+
+/*--------------------------------------------------------------------------------------
+	 Class Name: Recordset
+--------------------------------------------------------------------------------------*/
+
+
+class ADORecordSet_mysql extends ADORecordSet{
+
+	var $databaseType = "mysql";
+	var $canSeek = true;
+
+	function __construct($queryID,$mode=false)
+	{
+		if ($mode === false) {
+			global $ADODB_FETCH_MODE;
+			$mode = $ADODB_FETCH_MODE;
+		}
+		switch ($mode)
+		{
+		case ADODB_FETCH_NUM: $this->fetchMode = MYSQL_NUM; break;
+		case ADODB_FETCH_ASSOC:$this->fetchMode = MYSQL_ASSOC; break;
+		case ADODB_FETCH_DEFAULT:
+		case ADODB_FETCH_BOTH:
+		default:
+			$this->fetchMode = MYSQL_BOTH; break;
+		}
+		$this->adodbFetchMode = $mode;
+		parent::__construct($queryID);
+	}
+
+	function _initrs()
+	{
+	//GLOBAL $ADODB_COUNTRECS;
+	//	$this->_numOfRows = ($ADODB_COUNTRECS) ? @mysql_num_rows($this->_queryID):-1;
+		$this->_numOfRows = @mysql_num_rows($this->_queryID);
+		$this->_numOfFields = @mysql_num_fields($this->_queryID);
+	}
+
+	function FetchField($fieldOffset = -1)
+	{
+		if ($fieldOffset != -1) {
+			$o = @mysql_fetch_field($this->_queryID, $fieldOffset);
+			$f = @mysql_field_flags($this->_queryID,$fieldOffset);
+			if ($o) $o->max_length = @mysql_field_len($this->_queryID,$fieldOffset); // suggested by: Jim Nicholson (jnich#att.com)
+			//$o->max_length = -1; // mysql returns the max length less spaces -- so it is unrealiable
+			if ($o) $o->binary = (strpos($f,'binary')!== false);
+		}
+		else {	/*	The $fieldOffset argument is not provided thus its -1 	*/
+			$o = @mysql_fetch_field($this->_queryID);
+			//if ($o) $o->max_length = @mysql_field_len($this->_queryID); // suggested by: Jim Nicholson (jnich#att.com)
+			$o->max_length = -1; // mysql returns the max length less spaces -- so it is unrealiable
+		}
+
+		return $o;
+	}
+
+	function GetRowAssoc($upper = ADODB_ASSOC_CASE)
+	{
+		if ($this->fetchMode == MYSQL_ASSOC && $upper == ADODB_ASSOC_CASE_LOWER) {
+			$row = $this->fields;
+		}
+		else {
+			$row = ADORecordSet::GetRowAssoc($upper);
+		}
+		return $row;
+	}
+
+	/* Use associative array to get fields array */
+	function Fields($colname)
+	{
+		// added @ by "Michael William Miller" 
+		if ($this->fetchMode != MYSQL_NUM) return @$this->fields[$colname];
+
+		if (!$this->bind) {
+			$this->bind = array();
+			for ($i=0; $i < $this->_numOfFields; $i++) {
+				$o = $this->FetchField($i);
+				$this->bind[strtoupper($o->name)] = $i;
+			}
+		}
+		 return $this->fields[$this->bind[strtoupper($colname)]];
+	}
+
+	function _seek($row)
+	{
+		if ($this->_numOfRows == 0) return false;
+		return @mysql_data_seek($this->_queryID,$row);
+	}
+
+	function MoveNext()
+	{
+		//return adodb_movenext($this);
+		//if (defined('ADODB_EXTENSION')) return adodb_movenext($this);
+		if (@$this->fields = mysql_fetch_array($this->_queryID,$this->fetchMode)) {
+			$this->_updatefields();
+			$this->_currentRow += 1;
+			return true;
+		}
+		if (!$this->EOF) {
+			$this->_currentRow += 1;
+			$this->EOF = true;
+		}
+		return false;
+	}
+
+	function _fetch()
+	{
+		$this->fields = @mysql_fetch_array($this->_queryID,$this->fetchMode);
+		$this->_updatefields();
+		return is_array($this->fields);
+	}
+
+	function _close() {
+		@mysql_free_result($this->_queryID);
+		$this->_queryID = false;
+	}
+
+	function MetaType($t,$len=-1,$fieldobj=false)
+	{
+		if (is_object($t)) {
+			$fieldobj = $t;
+			$t = $fieldobj->type;
+			$len = $fieldobj->max_length;
+		}
+
+		$len = -1; // mysql max_length is not accurate
+		switch (strtoupper($t)) {
+		case 'STRING':
+		case 'CHAR':
+		case 'VARCHAR':
+		case 'TINYBLOB':
+		case 'TINYTEXT':
+		case 'ENUM':
+		case 'SET':
+			if ($len <= $this->blobSize) return 'C';
+
+		case 'TEXT':
+		case 'LONGTEXT':
+		case 'MEDIUMTEXT':
+			return 'X';
+
+		// php_mysql extension always returns 'blob' even if 'text'
+		// so we have to check whether binary...
+		case 'IMAGE':
+		case 'LONGBLOB':
+		case 'BLOB':
+		case 'MEDIUMBLOB':
+		case 'BINARY':
+			return !empty($fieldobj->binary) ? 'B' : 'X';
+
+		case 'YEAR':
+		case 'DATE': return 'D';
+
+		case 'TIME':
+		case 'DATETIME':
+		case 'TIMESTAMP': return 'T';
+
+		case 'INT':
+		case 'INTEGER':
+		case 'BIGINT':
+		case 'TINYINT':
+		case 'MEDIUMINT':
+		case 'SMALLINT':
+
+			if (!empty($fieldobj->primary_key)) return 'R';
+			else return 'I';
+
+		default: return 'N';
+		}
+	}
+
+}
+
+class ADORecordSet_ext_mysql extends ADORecordSet_mysql {
+	function __construct($queryID,$mode=false)
+	{
+		parent::__construct($queryID,$mode);
+	}
+
+	function MoveNext()
+	{
+		return @adodb_movenext($this);
+	}
+}
+
+}
diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-mysqli.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-mysqli.inc.php
new file mode 100644
index 000000000..188efc9fe
--- /dev/null
+++ b/app/vendor/adodb/adodb-php/drivers/adodb-mysqli.inc.php
@@ -0,0 +1,1293 @@
+_transmode = $transaction_mode;
+		if (empty($transaction_mode)) {
+			$this->Execute('SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ');
+			return;
+		}
+		if (!stristr($transaction_mode,'isolation')) $transaction_mode = 'ISOLATION LEVEL '.$transaction_mode;
+		$this->Execute("SET SESSION TRANSACTION ".$transaction_mode);
+	}
+
+	// returns true or false
+	// To add: parameter int $port,
+	//         parameter string $socket
+	function _connect($argHostname = NULL,
+				$argUsername = NULL,
+				$argPassword = NULL,
+				$argDatabasename = NULL, $persist=false)
+	{
+		if(!extension_loaded("mysqli")) {
+			return null;
+		}
+		$this->_connectionID = @mysqli_init();
+
+		if (is_null($this->_connectionID)) {
+			// mysqli_init only fails if insufficient memory
+			if ($this->debug) {
+				ADOConnection::outp("mysqli_init() failed : "  . $this->ErrorMsg());
+			}
+			return false;
+		}
+		/*
+		I suggest a simple fix which would enable adodb and mysqli driver to
+		read connection options from the standard mysql configuration file
+		/etc/my.cnf - "Bastien Duclaux" 
+		*/
+		foreach($this->optionFlags as $arr) {
+			mysqli_options($this->_connectionID,$arr[0],$arr[1]);
+		}
+
+		//http ://php.net/manual/en/mysqli.persistconns.php
+		if ($persist && PHP_VERSION > 5.2 && strncmp($argHostname,'p:',2) != 0) $argHostname = 'p:'.$argHostname;
+
+		#if (!empty($this->port)) $argHostname .= ":".$this->port;
+		$ok = mysqli_real_connect($this->_connectionID,
+					$argHostname,
+					$argUsername,
+					$argPassword,
+					$argDatabasename,
+					# PHP7 compat: port must be int. Use default port if cast yields zero
+					(int)$this->port != 0 ? (int)$this->port : 3306,
+					$this->socket,
+					$this->clientFlags);
+
+		if ($ok) {
+			if ($argDatabasename)  return $this->SelectDB($argDatabasename);
+			return true;
+		} else {
+			if ($this->debug) {
+				ADOConnection::outp("Could't connect : "  . $this->ErrorMsg());
+			}
+			$this->_connectionID = null;
+			return false;
+		}
+	}
+
+	// returns true or false
+	// How to force a persistent connection
+	function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
+	{
+		return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabasename, true);
+	}
+
+	// When is this used? Close old connection first?
+	// In _connect(), check $this->forceNewConnect?
+	function _nconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
+	{
+		$this->forceNewConnect = true;
+		return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabasename);
+	}
+
+	function IfNull( $field, $ifNull )
+	{
+		return " IFNULL($field, $ifNull) "; // if MySQL
+	}
+
+	// do not use $ADODB_COUNTRECS
+	function GetOne($sql,$inputarr=false)
+	{
+		global $ADODB_GETONE_EOF;
+
+		$ret = false;
+		$rs = $this->Execute($sql,$inputarr);
+		if ($rs) {
+			if ($rs->EOF) $ret = $ADODB_GETONE_EOF;
+			else $ret = reset($rs->fields);
+			$rs->Close();
+		}
+		return $ret;
+	}
+
+	function ServerInfo()
+	{
+		$arr['description'] = $this->GetOne("select version()");
+		$arr['version'] = ADOConnection::_findvers($arr['description']);
+		return $arr;
+	}
+
+
+	function BeginTrans()
+	{
+		if ($this->transOff) return true;
+		$this->transCnt += 1;
+
+		//$this->Execute('SET AUTOCOMMIT=0');
+		mysqli_autocommit($this->_connectionID, false);
+		$this->Execute('BEGIN');
+		return true;
+	}
+
+	function CommitTrans($ok=true)
+	{
+		if ($this->transOff) return true;
+		if (!$ok) return $this->RollbackTrans();
+
+		if ($this->transCnt) $this->transCnt -= 1;
+		$this->Execute('COMMIT');
+
+		//$this->Execute('SET AUTOCOMMIT=1');
+		mysqli_autocommit($this->_connectionID, true);
+		return true;
+	}
+
+	function RollbackTrans()
+	{
+		if ($this->transOff) return true;
+		if ($this->transCnt) $this->transCnt -= 1;
+		$this->Execute('ROLLBACK');
+		//$this->Execute('SET AUTOCOMMIT=1');
+		mysqli_autocommit($this->_connectionID, true);
+		return true;
+	}
+
+	function RowLock($tables,$where='',$col='1 as adodbignore')
+	{
+		if ($this->transCnt==0) $this->BeginTrans();
+		if ($where) $where = ' where '.$where;
+		$rs = $this->Execute("select $col from $tables $where for update");
+		return !empty($rs);
+	}
+
+	/**
+	 * Quotes a string to be sent to the database
+	 * When there is no active connection,
+	 * @param string $s The string to quote
+	 * @param boolean $magic_quotes If false, use mysqli_real_escape_string()
+	 *     if you are quoting a string extracted from a POST/GET variable,
+	 *     then pass get_magic_quotes_gpc() as the second parameter. This will
+	 *     ensure that the variable is not quoted twice, once by qstr() and
+	 *     once by the magic_quotes_gpc.
+	 *     Eg. $s = $db->qstr(_GET['name'],get_magic_quotes_gpc());
+	 * @return string Quoted string
+	 */
+	function qstr($s, $magic_quotes = false)
+	{
+		if (is_null($s)) return 'NULL';
+		if (!$magic_quotes) {
+			// mysqli_real_escape_string() throws a warning when the given
+			// connection is invalid
+			if (PHP_VERSION >= 5 && $this->_connectionID) {
+				return "'" . mysqli_real_escape_string($this->_connectionID, $s) . "'";
+			}
+
+			if ($this->replaceQuote[0] == '\\') {
+				$s = adodb_str_replace(array('\\',"\0"), array('\\\\',"\\\0") ,$s);
+			}
+			return "'" . str_replace("'", $this->replaceQuote, $s) . "'";
+		}
+		// undo magic quotes for "
+		$s = str_replace('\\"','"',$s);
+		return "'$s'";
+	}
+
+	function _insertid()
+	{
+		$result = @mysqli_insert_id($this->_connectionID);
+		if ($result == -1) {
+			if ($this->debug) ADOConnection::outp("mysqli_insert_id() failed : "  . $this->ErrorMsg());
+		}
+		return $result;
+	}
+
+	// Only works for INSERT, UPDATE and DELETE query's
+	function _affectedrows()
+	{
+		$result =  @mysqli_affected_rows($this->_connectionID);
+		if ($result == -1) {
+			if ($this->debug) ADOConnection::outp("mysqli_affected_rows() failed : "  . $this->ErrorMsg());
+		}
+		return $result;
+	}
+
+	// See http://www.mysql.com/doc/M/i/Miscellaneous_functions.html
+	// Reference on Last_Insert_ID on the recommended way to simulate sequences
+	var $_genIDSQL = "update %s set id=LAST_INSERT_ID(id+1);";
+	var $_genSeqSQL = "create table if not exists %s (id int not null)";
+	var $_genSeqCountSQL = "select count(*) from %s";
+	var $_genSeq2SQL = "insert into %s values (%s)";
+	var $_dropSeqSQL = "drop table if exists %s";
+
+	function CreateSequence($seqname='adodbseq',$startID=1)
+	{
+		if (empty($this->_genSeqSQL)) return false;
+		$u = strtoupper($seqname);
+
+		$ok = $this->Execute(sprintf($this->_genSeqSQL,$seqname));
+		if (!$ok) return false;
+		return $this->Execute(sprintf($this->_genSeq2SQL,$seqname,$startID-1));
+	}
+
+	function GenID($seqname='adodbseq',$startID=1)
+	{
+		// post-nuke sets hasGenID to false
+		if (!$this->hasGenID) return false;
+
+		$getnext = sprintf($this->_genIDSQL,$seqname);
+		$holdtransOK = $this->_transOK; // save the current status
+		$rs = @$this->Execute($getnext);
+		if (!$rs) {
+			if ($holdtransOK) $this->_transOK = true; //if the status was ok before reset
+			$u = strtoupper($seqname);
+			$this->Execute(sprintf($this->_genSeqSQL,$seqname));
+			$cnt = $this->GetOne(sprintf($this->_genSeqCountSQL,$seqname));
+			if (!$cnt) $this->Execute(sprintf($this->_genSeq2SQL,$seqname,$startID-1));
+			$rs = $this->Execute($getnext);
+		}
+
+		if ($rs) {
+			$this->genID = mysqli_insert_id($this->_connectionID);
+			$rs->Close();
+		} else
+			$this->genID = 0;
+
+		return $this->genID;
+	}
+
+	function MetaDatabases()
+	{
+		$query = "SHOW DATABASES";
+		$ret = $this->Execute($query);
+		if ($ret && is_object($ret)){
+			$arr = array();
+			while (!$ret->EOF){
+				$db = $ret->Fields('Database');
+				if ($db != 'mysql') $arr[] = $db;
+				$ret->MoveNext();
+			}
+			return $arr;
+		}
+		return $ret;
+	}
+
+
+	function MetaIndexes ($table, $primary = FALSE, $owner = false)
+	{
+		// save old fetch mode
+		global $ADODB_FETCH_MODE;
+
+		$false = false;
+		$save = $ADODB_FETCH_MODE;
+		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
+		if ($this->fetchMode !== FALSE) {
+			$savem = $this->SetFetchMode(FALSE);
+		}
+
+		// get index details
+		$rs = $this->Execute(sprintf('SHOW INDEXES FROM %s',$table));
+
+		// restore fetchmode
+		if (isset($savem)) {
+			$this->SetFetchMode($savem);
+		}
+		$ADODB_FETCH_MODE = $save;
+
+		if (!is_object($rs)) {
+			return $false;
+		}
+
+		$indexes = array ();
+
+		// parse index data into array
+		while ($row = $rs->FetchRow()) {
+			if ($primary == FALSE AND $row[2] == 'PRIMARY') {
+				continue;
+			}
+
+			if (!isset($indexes[$row[2]])) {
+				$indexes[$row[2]] = array(
+					'unique' => ($row[1] == 0),
+					'columns' => array()
+				);
+			}
+
+			$indexes[$row[2]]['columns'][$row[3] - 1] = $row[4];
+		}
+
+		// sort columns by order in the index
+		foreach ( array_keys ($indexes) as $index )
+		{
+			ksort ($indexes[$index]['columns']);
+		}
+
+		return $indexes;
+	}
+
+
+	// Format date column in sql string given an input format that understands Y M D
+	function SQLDate($fmt, $col=false)
+	{
+		if (!$col) $col = $this->sysTimeStamp;
+		$s = 'DATE_FORMAT('.$col.",'";
+		$concat = false;
+		$len = strlen($fmt);
+		for ($i=0; $i < $len; $i++) {
+			$ch = $fmt[$i];
+			switch($ch) {
+			case 'Y':
+			case 'y':
+				$s .= '%Y';
+				break;
+			case 'Q':
+			case 'q':
+				$s .= "'),Quarter($col)";
+
+				if ($len > $i+1) $s .= ",DATE_FORMAT($col,'";
+				else $s .= ",('";
+				$concat = true;
+				break;
+			case 'M':
+				$s .= '%b';
+				break;
+
+			case 'm':
+				$s .= '%m';
+				break;
+			case 'D':
+			case 'd':
+				$s .= '%d';
+				break;
+
+			case 'H':
+				$s .= '%H';
+				break;
+
+			case 'h':
+				$s .= '%I';
+				break;
+
+			case 'i':
+				$s .= '%i';
+				break;
+
+			case 's':
+				$s .= '%s';
+				break;
+
+			case 'a':
+			case 'A':
+				$s .= '%p';
+				break;
+
+			case 'w':
+				$s .= '%w';
+				break;
+
+			case 'l':
+				$s .= '%W';
+				break;
+
+			default:
+
+				if ($ch == '\\') {
+					$i++;
+					$ch = substr($fmt,$i,1);
+				}
+				$s .= $ch;
+				break;
+			}
+		}
+		$s.="')";
+		if ($concat) $s = "CONCAT($s)";
+		return $s;
+	}
+
+	// returns concatenated string
+	// much easier to run "mysqld --ansi" or "mysqld --sql-mode=PIPES_AS_CONCAT" and use || operator
+	function Concat()
+	{
+		$s = "";
+		$arr = func_get_args();
+
+		// suggestion by andrew005@mnogo.ru
+		$s = implode(',',$arr);
+		if (strlen($s) > 0) return "CONCAT($s)";
+		else return '';
+	}
+
+	// dayFraction is a day in floating point
+	function OffsetDate($dayFraction,$date=false)
+	{
+		if (!$date) $date = $this->sysDate;
+
+		$fraction = $dayFraction * 24 * 3600;
+		return $date . ' + INTERVAL ' .	 $fraction.' SECOND';
+
+//		return "from_unixtime(unix_timestamp($date)+$fraction)";
+	}
+
+	function MetaProcedures($NamePattern = false, $catalog  = null, $schemaPattern  = null)
+	{
+		// save old fetch mode
+		global $ADODB_FETCH_MODE;
+
+		$false = false;
+		$save = $ADODB_FETCH_MODE;
+		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
+
+		if ($this->fetchMode !== FALSE) {
+			$savem = $this->SetFetchMode(FALSE);
+		}
+
+		$procedures = array ();
+
+		// get index details
+
+		$likepattern = '';
+		if ($NamePattern) {
+			$likepattern = " LIKE '".$NamePattern."'";
+		}
+		$rs = $this->Execute('SHOW PROCEDURE STATUS'.$likepattern);
+		if (is_object($rs)) {
+
+			// parse index data into array
+			while ($row = $rs->FetchRow()) {
+				$procedures[$row[1]] = array(
+					'type' => 'PROCEDURE',
+					'catalog' => '',
+					'schema' => '',
+					'remarks' => $row[7],
+				);
+			}
+		}
+
+		$rs = $this->Execute('SHOW FUNCTION STATUS'.$likepattern);
+		if (is_object($rs)) {
+			// parse index data into array
+			while ($row = $rs->FetchRow()) {
+				$procedures[$row[1]] = array(
+					'type' => 'FUNCTION',
+					'catalog' => '',
+					'schema' => '',
+					'remarks' => $row[7]
+				);
+			}
+		}
+
+		// restore fetchmode
+		if (isset($savem)) {
+				$this->SetFetchMode($savem);
+		}
+		$ADODB_FETCH_MODE = $save;
+
+		return $procedures;
+	}
+
+	/**
+	 * Retrieves a list of tables based on given criteria
+	 *
+	 * @param string $ttype Table type = 'TABLE', 'VIEW' or false=both (default)
+	 * @param string $showSchema schema name, false = current schema (default)
+	 * @param string $mask filters the table by name
+	 *
+	 * @return array list of tables
+	 */
+	function MetaTables($ttype=false,$showSchema=false,$mask=false)
+	{
+		$save = $this->metaTablesSQL;
+		if ($showSchema && is_string($showSchema)) {
+			$this->metaTablesSQL .= $this->qstr($showSchema);
+		} else {
+			$this->metaTablesSQL .= "schema()";
+		}
+
+		if ($mask) {
+			$mask = $this->qstr($mask);
+			$this->metaTablesSQL .= " AND table_name LIKE $mask";
+		}
+		$ret = ADOConnection::MetaTables($ttype,$showSchema);
+
+		$this->metaTablesSQL = $save;
+		return $ret;
+	}
+
+	// "Innox - Juan Carlos Gonzalez" 
+	function MetaForeignKeys( $table, $owner = FALSE, $upper = FALSE, $associative = FALSE )
+	{
+	 global $ADODB_FETCH_MODE;
+
+		if ($ADODB_FETCH_MODE == ADODB_FETCH_ASSOC || $this->fetchMode == ADODB_FETCH_ASSOC) $associative = true;
+
+		if ( !empty($owner) ) {
+			$table = "$owner.$table";
+		}
+		$a_create_table = $this->getRow(sprintf('SHOW CREATE TABLE %s', $table));
+		if ($associative) {
+			$create_sql = isset($a_create_table["Create Table"]) ? $a_create_table["Create Table"] : $a_create_table["Create View"];
+		} else $create_sql = $a_create_table[1];
+
+		$matches = array();
+
+		if (!preg_match_all("/FOREIGN KEY \(`(.*?)`\) REFERENCES `(.*?)` \(`(.*?)`\)/", $create_sql, $matches)) return false;
+		$foreign_keys = array();
+		$num_keys = count($matches[0]);
+		for ( $i = 0; $i < $num_keys; $i ++ ) {
+			$my_field  = explode('`, `', $matches[1][$i]);
+			$ref_table = $matches[2][$i];
+			$ref_field = explode('`, `', $matches[3][$i]);
+
+			if ( $upper ) {
+				$ref_table = strtoupper($ref_table);
+			}
+
+			// see https://sourceforge.net/tracker/index.php?func=detail&aid=2287278&group_id=42718&atid=433976
+			if (!isset($foreign_keys[$ref_table])) {
+				$foreign_keys[$ref_table] = array();
+			}
+			$num_fields = count($my_field);
+			for ( $j = 0; $j < $num_fields; $j ++ ) {
+				if ( $associative ) {
+					$foreign_keys[$ref_table][$ref_field[$j]] = $my_field[$j];
+				} else {
+					$foreign_keys[$ref_table][] = "{$my_field[$j]}={$ref_field[$j]}";
+				}
+			}
+		}
+
+		return $foreign_keys;
+	}
+
+	function MetaColumns($table, $normalize=true)
+	{
+		$false = false;
+		if (!$this->metaColumnsSQL)
+			return $false;
+
+		global $ADODB_FETCH_MODE;
+		$save = $ADODB_FETCH_MODE;
+		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
+		if ($this->fetchMode !== false)
+			$savem = $this->SetFetchMode(false);
+		$rs = $this->Execute(sprintf($this->metaColumnsSQL,$table));
+		if (isset($savem)) $this->SetFetchMode($savem);
+		$ADODB_FETCH_MODE = $save;
+		if (!is_object($rs))
+			return $false;
+
+		$retarr = array();
+		while (!$rs->EOF) {
+			$fld = new ADOFieldObject();
+			$fld->name = $rs->fields[0];
+			$type = $rs->fields[1];
+
+			// split type into type(length):
+			$fld->scale = null;
+			if (preg_match("/^(.+)\((\d+),(\d+)/", $type, $query_array)) {
+				$fld->type = $query_array[1];
+				$fld->max_length = is_numeric($query_array[2]) ? $query_array[2] : -1;
+				$fld->scale = is_numeric($query_array[3]) ? $query_array[3] : -1;
+			} elseif (preg_match("/^(.+)\((\d+)/", $type, $query_array)) {
+				$fld->type = $query_array[1];
+				$fld->max_length = is_numeric($query_array[2]) ? $query_array[2] : -1;
+			} elseif (preg_match("/^(enum)\((.*)\)$/i", $type, $query_array)) {
+				$fld->type = $query_array[1];
+				$arr = explode(",",$query_array[2]);
+				$fld->enums = $arr;
+				$zlen = max(array_map("strlen",$arr)) - 2; // PHP >= 4.0.6
+				$fld->max_length = ($zlen > 0) ? $zlen : 1;
+			} else {
+				$fld->type = $type;
+				$fld->max_length = -1;
+			}
+			$fld->not_null = ($rs->fields[2] != 'YES');
+			$fld->primary_key = ($rs->fields[3] == 'PRI');
+			$fld->auto_increment = (strpos($rs->fields[5], 'auto_increment') !== false);
+			$fld->binary = (strpos($type,'blob') !== false);
+			$fld->unsigned = (strpos($type,'unsigned') !== false);
+			$fld->zerofill = (strpos($type,'zerofill') !== false);
+
+			if (!$fld->binary) {
+				$d = $rs->fields[4];
+				if ($d != '' && $d != 'NULL') {
+					$fld->has_default = true;
+					$fld->default_value = $d;
+				} else {
+					$fld->has_default = false;
+				}
+			}
+
+			if ($save == ADODB_FETCH_NUM) {
+				$retarr[] = $fld;
+			} else {
+				$retarr[strtoupper($fld->name)] = $fld;
+			}
+			$rs->MoveNext();
+		}
+
+		$rs->Close();
+		return $retarr;
+	}
+
+	// returns true or false
+	function SelectDB($dbName)
+	{
+//		$this->_connectionID = $this->mysqli_resolve_link($this->_connectionID);
+		$this->database = $dbName;
+		$this->databaseName = $dbName; # obsolete, retained for compat with older adodb versions
+
+		if ($this->_connectionID) {
+			$result = @mysqli_select_db($this->_connectionID, $dbName);
+			if (!$result) {
+				ADOConnection::outp("Select of database " . $dbName . " failed. " . $this->ErrorMsg());
+			}
+			return $result;
+		}
+		return false;
+	}
+
+	// parameters use PostgreSQL convention, not MySQL
+	function SelectLimit($sql,
+				$nrows = -1,
+				$offset = -1,
+				$inputarr = false,
+				$secs = 0)
+	{
+		$offsetStr = ($offset >= 0) ? "$offset," : '';
+		if ($nrows < 0) $nrows = '18446744073709551615';
+
+		if ($secs)
+			$rs = $this->CacheExecute($secs, $sql . " LIMIT $offsetStr$nrows" , $inputarr );
+		else
+			$rs = $this->Execute($sql . " LIMIT $offsetStr$nrows" , $inputarr );
+
+		return $rs;
+	}
+
+
+	function Prepare($sql)
+	{
+		return $sql;
+		$stmt = $this->_connectionID->prepare($sql);
+		if (!$stmt) {
+			echo $this->ErrorMsg();
+			return $sql;
+		}
+		return array($sql,$stmt);
+	}
+
+
+	// returns queryID or false
+	function _query($sql, $inputarr)
+	{
+	global $ADODB_COUNTRECS;
+		// Move to the next recordset, or return false if there is none. In a stored proc
+		// call, mysqli_next_result returns true for the last "recordset", but mysqli_store_result
+		// returns false. I think this is because the last "recordset" is actually just the
+		// return value of the stored proc (ie the number of rows affected).
+		// Commented out for reasons of performance. You should retrieve every recordset yourself.
+		//	if (!mysqli_next_result($this->connection->_connectionID))	return false;
+
+		if (is_array($sql)) {
+
+			// Prepare() not supported because mysqli_stmt_execute does not return a recordset, but
+			// returns as bound variables.
+
+			$stmt = $sql[1];
+			$a = '';
+			foreach($inputarr as $k => $v) {
+				if (is_string($v)) $a .= 's';
+				else if (is_integer($v)) $a .= 'i';
+				else $a .= 'd';
+			}
+
+			$fnarr = array_merge( array($stmt,$a) , $inputarr);
+			$ret = call_user_func_array('mysqli_stmt_bind_param',$fnarr);
+			$ret = mysqli_stmt_execute($stmt);
+			return $ret;
+		}
+
+		/*
+		if (!$mysql_res =  mysqli_query($this->_connectionID, $sql, ($ADODB_COUNTRECS) ? MYSQLI_STORE_RESULT : MYSQLI_USE_RESULT)) {
+			if ($this->debug) ADOConnection::outp("Query: " . $sql . " failed. " . $this->ErrorMsg());
+			return false;
+		}
+
+		return $mysql_res;
+		*/
+
+		if ($this->multiQuery) {
+			$rs = mysqli_multi_query($this->_connectionID, $sql.';');
+			if ($rs) {
+				$rs = ($ADODB_COUNTRECS) ? @mysqli_store_result( $this->_connectionID ) : @mysqli_use_result( $this->_connectionID );
+				return $rs ? $rs : true; // mysqli_more_results( $this->_connectionID )
+			}
+		} else {
+			$rs = mysqli_query($this->_connectionID, $sql, $ADODB_COUNTRECS ? MYSQLI_STORE_RESULT : MYSQLI_USE_RESULT);
+
+			if ($rs) return $rs;
+		}
+
+		if($this->debug)
+			ADOConnection::outp("Query: " . $sql . " failed. " . $this->ErrorMsg());
+
+		return false;
+
+	}
+
+	/*	Returns: the last error message from previous database operation	*/
+	function ErrorMsg()
+	{
+		if (empty($this->_connectionID))
+			$this->_errorMsg = @mysqli_connect_error();
+		else
+			$this->_errorMsg = @mysqli_error($this->_connectionID);
+		return $this->_errorMsg;
+	}
+
+	/*	Returns: the last error number from previous database operation	*/
+	function ErrorNo()
+	{
+		if (empty($this->_connectionID))
+			return @mysqli_connect_errno();
+		else
+			return @mysqli_errno($this->_connectionID);
+	}
+
+	// returns true or false
+	function _close()
+	{
+		@mysqli_close($this->_connectionID);
+		$this->_connectionID = false;
+	}
+
+	/*
+	* Maximum size of C field
+	*/
+	function CharMax()
+	{
+		return 255;
+	}
+
+	/*
+	* Maximum size of X field
+	*/
+	function TextMax()
+	{
+		return 4294967295;
+	}
+
+
+	// this is a set of functions for managing client encoding - very important if the encodings
+	// of your database and your output target (i.e. HTML) don't match
+	// for instance, you may have UTF8 database and server it on-site as latin1 etc.
+	// GetCharSet - get the name of the character set the client is using now
+	// Under Windows, the functions should work with MySQL 4.1.11 and above, the set of charsets supported
+	// depends on compile flags of mysql distribution
+
+	function GetCharSet()
+	{
+		//we will use ADO's builtin property charSet
+		if (!method_exists($this->_connectionID,'character_set_name'))
+			return false;
+
+		$this->charSet = @$this->_connectionID->character_set_name();
+		if (!$this->charSet) {
+			return false;
+		} else {
+			return $this->charSet;
+		}
+	}
+
+	// SetCharSet - switch the client encoding
+	function SetCharSet($charset_name)
+	{
+		if (!method_exists($this->_connectionID,'set_charset')) {
+			return false;
+		}
+
+		if ($this->charSet !== $charset_name) {
+			$if = @$this->_connectionID->set_charset($charset_name);
+			return ($if === true & $this->GetCharSet() == $charset_name);
+		} else {
+			return true;
+		}
+	}
+
+}
+
+/*--------------------------------------------------------------------------------------
+	 Class Name: Recordset
+--------------------------------------------------------------------------------------*/
+
+class ADORecordSet_mysqli extends ADORecordSet{
+
+	var $databaseType = "mysqli";
+	var $canSeek = true;
+
+	function __construct($queryID, $mode = false)
+	{
+		if ($mode === false) {
+			global $ADODB_FETCH_MODE;
+			$mode = $ADODB_FETCH_MODE;
+		}
+
+		switch ($mode) {
+			case ADODB_FETCH_NUM:
+				$this->fetchMode = MYSQLI_NUM;
+				break;
+			case ADODB_FETCH_ASSOC:
+				$this->fetchMode = MYSQLI_ASSOC;
+				break;
+			case ADODB_FETCH_DEFAULT:
+			case ADODB_FETCH_BOTH:
+			default:
+				$this->fetchMode = MYSQLI_BOTH;
+				break;
+		}
+		$this->adodbFetchMode = $mode;
+		parent::__construct($queryID);
+	}
+
+	function _initrs()
+	{
+	global $ADODB_COUNTRECS;
+
+		$this->_numOfRows = $ADODB_COUNTRECS ? @mysqli_num_rows($this->_queryID) : -1;
+		$this->_numOfFields = @mysqli_num_fields($this->_queryID);
+	}
+
+/*
+1      = MYSQLI_NOT_NULL_FLAG
+2      = MYSQLI_PRI_KEY_FLAG
+4      = MYSQLI_UNIQUE_KEY_FLAG
+8      = MYSQLI_MULTIPLE_KEY_FLAG
+16     = MYSQLI_BLOB_FLAG
+32     = MYSQLI_UNSIGNED_FLAG
+64     = MYSQLI_ZEROFILL_FLAG
+128    = MYSQLI_BINARY_FLAG
+256    = MYSQLI_ENUM_FLAG
+512    = MYSQLI_AUTO_INCREMENT_FLAG
+1024   = MYSQLI_TIMESTAMP_FLAG
+2048   = MYSQLI_SET_FLAG
+32768  = MYSQLI_NUM_FLAG
+16384  = MYSQLI_PART_KEY_FLAG
+32768  = MYSQLI_GROUP_FLAG
+65536  = MYSQLI_UNIQUE_FLAG
+131072 = MYSQLI_BINCMP_FLAG
+*/
+
+	function FetchField($fieldOffset = -1)
+	{
+		$fieldnr = $fieldOffset;
+		if ($fieldOffset != -1) {
+			$fieldOffset = @mysqli_field_seek($this->_queryID, $fieldnr);
+		}
+		$o = @mysqli_fetch_field($this->_queryID);
+		if (!$o) return false;
+
+		//Fix for HHVM
+		if ( !isset($o->flags) ) {
+			$o->flags = 0;
+		}
+		/* Properties of an ADOFieldObject as set by MetaColumns */
+		$o->primary_key = $o->flags & MYSQLI_PRI_KEY_FLAG;
+		$o->not_null = $o->flags & MYSQLI_NOT_NULL_FLAG;
+		$o->auto_increment = $o->flags & MYSQLI_AUTO_INCREMENT_FLAG;
+		$o->binary = $o->flags & MYSQLI_BINARY_FLAG;
+		// $o->blob = $o->flags & MYSQLI_BLOB_FLAG; /* not returned by MetaColumns */
+		$o->unsigned = $o->flags & MYSQLI_UNSIGNED_FLAG;
+
+		return $o;
+	}
+
+	function GetRowAssoc($upper = ADODB_ASSOC_CASE)
+	{
+		if ($this->fetchMode == MYSQLI_ASSOC && $upper == ADODB_ASSOC_CASE_LOWER) {
+			return $this->fields;
+		}
+		$row = ADORecordSet::GetRowAssoc($upper);
+		return $row;
+	}
+
+	/* Use associative array to get fields array */
+	function Fields($colname)
+	{
+		if ($this->fetchMode != MYSQLI_NUM) {
+			return @$this->fields[$colname];
+		}
+
+		if (!$this->bind) {
+			$this->bind = array();
+			for ($i = 0; $i < $this->_numOfFields; $i++) {
+				$o = $this->FetchField($i);
+				$this->bind[strtoupper($o->name)] = $i;
+			}
+		}
+		return $this->fields[$this->bind[strtoupper($colname)]];
+	}
+
+	function _seek($row)
+	{
+		if ($this->_numOfRows == 0 || $row < 0) {
+			return false;
+		}
+
+		mysqli_data_seek($this->_queryID, $row);
+		$this->EOF = false;
+		return true;
+	}
+
+
+	function NextRecordSet()
+	{
+	global $ADODB_COUNTRECS;
+
+		mysqli_free_result($this->_queryID);
+		$this->_queryID = -1;
+		// Move to the next recordset, or return false if there is none. In a stored proc
+		// call, mysqli_next_result returns true for the last "recordset", but mysqli_store_result
+		// returns false. I think this is because the last "recordset" is actually just the
+		// return value of the stored proc (ie the number of rows affected).
+		if(!mysqli_next_result($this->connection->_connectionID)) {
+		return false;
+		}
+		// CD: There is no $this->_connectionID variable, at least in the ADO version I'm using
+		$this->_queryID = ($ADODB_COUNTRECS) ? @mysqli_store_result( $this->connection->_connectionID )
+						: @mysqli_use_result( $this->connection->_connectionID );
+		if(!$this->_queryID) {
+			return false;
+		}
+		$this->_inited = false;
+		$this->bind = false;
+		$this->_currentRow = -1;
+		$this->Init();
+		return true;
+	}
+
+	// 10% speedup to move MoveNext to child class
+	// This is the only implementation that works now (23-10-2003).
+	// Other functions return no or the wrong results.
+	function MoveNext()
+	{
+		if ($this->EOF) return false;
+		$this->_currentRow++;
+		$this->fields = @mysqli_fetch_array($this->_queryID,$this->fetchMode);
+
+		if (is_array($this->fields)) {
+			$this->_updatefields();
+			return true;
+		}
+		$this->EOF = true;
+		return false;
+	}
+
+	function _fetch()
+	{
+		$this->fields = mysqli_fetch_array($this->_queryID,$this->fetchMode);
+		$this->_updatefields();
+		return is_array($this->fields);
+	}
+
+	function _close()
+	{
+		//if results are attached to this pointer from Stored Proceedure calls, the next standard query will die 2014
+		//only a problem with persistant connections
+
+		if(isset($this->connection->_connectionID) && $this->connection->_connectionID) {
+			while(mysqli_more_results($this->connection->_connectionID)){
+				mysqli_next_result($this->connection->_connectionID);
+			}
+		}
+
+		if($this->_queryID instanceof mysqli_result) {
+			mysqli_free_result($this->_queryID);
+		}
+		$this->_queryID = false;
+	}
+
+/*
+
+0 = MYSQLI_TYPE_DECIMAL
+1 = MYSQLI_TYPE_CHAR
+1 = MYSQLI_TYPE_TINY
+2 = MYSQLI_TYPE_SHORT
+3 = MYSQLI_TYPE_LONG
+4 = MYSQLI_TYPE_FLOAT
+5 = MYSQLI_TYPE_DOUBLE
+6 = MYSQLI_TYPE_NULL
+7 = MYSQLI_TYPE_TIMESTAMP
+8 = MYSQLI_TYPE_LONGLONG
+9 = MYSQLI_TYPE_INT24
+10 = MYSQLI_TYPE_DATE
+11 = MYSQLI_TYPE_TIME
+12 = MYSQLI_TYPE_DATETIME
+13 = MYSQLI_TYPE_YEAR
+14 = MYSQLI_TYPE_NEWDATE
+247 = MYSQLI_TYPE_ENUM
+248 = MYSQLI_TYPE_SET
+249 = MYSQLI_TYPE_TINY_BLOB
+250 = MYSQLI_TYPE_MEDIUM_BLOB
+251 = MYSQLI_TYPE_LONG_BLOB
+252 = MYSQLI_TYPE_BLOB
+253 = MYSQLI_TYPE_VAR_STRING
+254 = MYSQLI_TYPE_STRING
+255 = MYSQLI_TYPE_GEOMETRY
+*/
+
+	function MetaType($t, $len = -1, $fieldobj = false)
+	{
+		if (is_object($t)) {
+			$fieldobj = $t;
+			$t = $fieldobj->type;
+			$len = $fieldobj->max_length;
+		}
+
+
+		$len = -1; // mysql max_length is not accurate
+		switch (strtoupper($t)) {
+		case 'STRING':
+		case 'CHAR':
+		case 'VARCHAR':
+		case 'TINYBLOB':
+		case 'TINYTEXT':
+		case 'ENUM':
+		case 'SET':
+
+		case MYSQLI_TYPE_TINY_BLOB :
+		#case MYSQLI_TYPE_CHAR :
+		case MYSQLI_TYPE_STRING :
+		case MYSQLI_TYPE_ENUM :
+		case MYSQLI_TYPE_SET :
+		case 253 :
+			if ($len <= $this->blobSize) return 'C';
+
+		case 'TEXT':
+		case 'LONGTEXT':
+		case 'MEDIUMTEXT':
+			return 'X';
+
+		// php_mysql extension always returns 'blob' even if 'text'
+		// so we have to check whether binary...
+		case 'IMAGE':
+		case 'LONGBLOB':
+		case 'BLOB':
+		case 'MEDIUMBLOB':
+
+		case MYSQLI_TYPE_BLOB :
+		case MYSQLI_TYPE_LONG_BLOB :
+		case MYSQLI_TYPE_MEDIUM_BLOB :
+			return !empty($fieldobj->binary) ? 'B' : 'X';
+
+		case 'YEAR':
+		case 'DATE':
+		case MYSQLI_TYPE_DATE :
+		case MYSQLI_TYPE_YEAR :
+			return 'D';
+
+		case 'TIME':
+		case 'DATETIME':
+		case 'TIMESTAMP':
+
+		case MYSQLI_TYPE_DATETIME :
+		case MYSQLI_TYPE_NEWDATE :
+		case MYSQLI_TYPE_TIME :
+		case MYSQLI_TYPE_TIMESTAMP :
+			return 'T';
+
+		case 'INT':
+		case 'INTEGER':
+		case 'BIGINT':
+		case 'TINYINT':
+		case 'MEDIUMINT':
+		case 'SMALLINT':
+
+		case MYSQLI_TYPE_INT24 :
+		case MYSQLI_TYPE_LONG :
+		case MYSQLI_TYPE_LONGLONG :
+		case MYSQLI_TYPE_SHORT :
+		case MYSQLI_TYPE_TINY :
+			if (!empty($fieldobj->primary_key)) return 'R';
+			return 'I';
+
+		// Added floating-point types
+		// Maybe not necessery.
+		case 'FLOAT':
+		case 'DOUBLE':
+//		case 'DOUBLE PRECISION':
+		case 'DECIMAL':
+		case 'DEC':
+		case 'FIXED':
+		default:
+			//if (!is_numeric($t)) echo "

--- Error in type matching $t -----

"; + return 'N'; + } + } // function + + +} // rs class + +} + +class ADORecordSet_array_mysqli extends ADORecordSet_array { + + function __construct($id=-1,$mode=false) + { + parent::__construct($id,$mode); + } + + function MetaType($t, $len = -1, $fieldobj = false) + { + if (is_object($t)) { + $fieldobj = $t; + $t = $fieldobj->type; + $len = $fieldobj->max_length; + } + + + $len = -1; // mysql max_length is not accurate + switch (strtoupper($t)) { + case 'STRING': + case 'CHAR': + case 'VARCHAR': + case 'TINYBLOB': + case 'TINYTEXT': + case 'ENUM': + case 'SET': + + case MYSQLI_TYPE_TINY_BLOB : + #case MYSQLI_TYPE_CHAR : + case MYSQLI_TYPE_STRING : + case MYSQLI_TYPE_ENUM : + case MYSQLI_TYPE_SET : + case 253 : + if ($len <= $this->blobSize) return 'C'; + + case 'TEXT': + case 'LONGTEXT': + case 'MEDIUMTEXT': + return 'X'; + + // php_mysql extension always returns 'blob' even if 'text' + // so we have to check whether binary... + case 'IMAGE': + case 'LONGBLOB': + case 'BLOB': + case 'MEDIUMBLOB': + + case MYSQLI_TYPE_BLOB : + case MYSQLI_TYPE_LONG_BLOB : + case MYSQLI_TYPE_MEDIUM_BLOB : + + return !empty($fieldobj->binary) ? 'B' : 'X'; + case 'YEAR': + case 'DATE': + case MYSQLI_TYPE_DATE : + case MYSQLI_TYPE_YEAR : + + return 'D'; + + case 'TIME': + case 'DATETIME': + case 'TIMESTAMP': + + case MYSQLI_TYPE_DATETIME : + case MYSQLI_TYPE_NEWDATE : + case MYSQLI_TYPE_TIME : + case MYSQLI_TYPE_TIMESTAMP : + + return 'T'; + + case 'INT': + case 'INTEGER': + case 'BIGINT': + case 'TINYINT': + case 'MEDIUMINT': + case 'SMALLINT': + + case MYSQLI_TYPE_INT24 : + case MYSQLI_TYPE_LONG : + case MYSQLI_TYPE_LONGLONG : + case MYSQLI_TYPE_SHORT : + case MYSQLI_TYPE_TINY : + + if (!empty($fieldobj->primary_key)) return 'R'; + + return 'I'; + + + // Added floating-point types + // Maybe not necessery. + case 'FLOAT': + case 'DOUBLE': +// case 'DOUBLE PRECISION': + case 'DECIMAL': + case 'DEC': + case 'FIXED': + default: + //if (!is_numeric($t)) echo "

--- Error in type matching $t -----

"; + return 'N'; + } + } // function + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-mysqlpo.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-mysqlpo.inc.php new file mode 100644 index 000000000..26b354aee --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-mysqlpo.inc.php @@ -0,0 +1,128 @@ + + + This driver extends the deprecated mysql driver, and was originally designed to be a + portable driver in the same manner as oci8po and mssqlpo. Its functionality + is exactly duplicated in the mysqlt driver, which is itself deprecated. + This driver will be removed in ADOdb version 6.0.0. + + Requires mysql client. Works on Windows and Unix. +*/ + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +include_once(ADODB_DIR."/drivers/adodb-mysql.inc.php"); + + +class ADODB_mysqlt extends ADODB_mysql { + var $databaseType = 'mysqlt'; + var $ansiOuter = true; // for Version 3.23.17 or later + var $hasTransactions = true; + var $autoRollback = true; // apparently mysql does not autorollback properly + + function __construct() + { + global $ADODB_EXTENSION; if ($ADODB_EXTENSION) $this->rsPrefix .= 'ext_'; + } + + function BeginTrans() + { + if ($this->transOff) return true; + $this->transCnt += 1; + $this->Execute('SET AUTOCOMMIT=0'); + $this->Execute('BEGIN'); + return true; + } + + function CommitTrans($ok=true) + { + if ($this->transOff) return true; + if (!$ok) return $this->RollbackTrans(); + + if ($this->transCnt) $this->transCnt -= 1; + $this->Execute('COMMIT'); + $this->Execute('SET AUTOCOMMIT=1'); + return true; + } + + function RollbackTrans() + { + if ($this->transOff) return true; + if ($this->transCnt) $this->transCnt -= 1; + $this->Execute('ROLLBACK'); + $this->Execute('SET AUTOCOMMIT=1'); + return true; + } + + function RowLock($tables,$where='',$col='1 as adodbignore') + { + if ($this->transCnt==0) $this->BeginTrans(); + if ($where) $where = ' where '.$where; + $rs = $this->Execute("select $col from $tables $where for update"); + return !empty($rs); + } + +} + +class ADORecordSet_mysqlt extends ADORecordSet_mysql{ + var $databaseType = "mysqlt"; + + function __construct($queryID,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + + switch ($mode) + { + case ADODB_FETCH_NUM: $this->fetchMode = MYSQL_NUM; break; + case ADODB_FETCH_ASSOC:$this->fetchMode = MYSQL_ASSOC; break; + + case ADODB_FETCH_DEFAULT: + case ADODB_FETCH_BOTH: + default: $this->fetchMode = MYSQL_BOTH; break; + } + + $this->adodbFetchMode = $mode; + parent::__construct($queryID); + } + + function MoveNext() + { + if (@$this->fields = mysql_fetch_array($this->_queryID,$this->fetchMode)) { + $this->_currentRow += 1; + return true; + } + if (!$this->EOF) { + $this->_currentRow += 1; + $this->EOF = true; + } + return false; + } +} + +class ADORecordSet_ext_mysqlt extends ADORecordSet_mysqlt { + + function __construct($queryID,$mode=false) + { + parent::__construct($queryID,$mode); + } + + function MoveNext() + { + return adodb_movenext($this); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-mysqlt.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-mysqlt.inc.php new file mode 100644 index 000000000..79c937600 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-mysqlt.inc.php @@ -0,0 +1,137 @@ +rsPrefix .= 'ext_'; + } + + /* set transaction mode + + SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL +{ READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE } + + */ + function SetTransactionMode( $transaction_mode ) + { + $this->_transmode = $transaction_mode; + if (empty($transaction_mode)) { + $this->Execute('SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ'); + return; + } + if (!stristr($transaction_mode,'isolation')) $transaction_mode = 'ISOLATION LEVEL '.$transaction_mode; + $this->Execute("SET SESSION TRANSACTION ".$transaction_mode); + } + + function BeginTrans() + { + if ($this->transOff) return true; + $this->transCnt += 1; + $this->Execute('SET AUTOCOMMIT=0'); + $this->Execute('BEGIN'); + return true; + } + + function CommitTrans($ok=true) + { + if ($this->transOff) return true; + if (!$ok) return $this->RollbackTrans(); + + if ($this->transCnt) $this->transCnt -= 1; + $ok = $this->Execute('COMMIT'); + $this->Execute('SET AUTOCOMMIT=1'); + return $ok ? true : false; + } + + function RollbackTrans() + { + if ($this->transOff) return true; + if ($this->transCnt) $this->transCnt -= 1; + $ok = $this->Execute('ROLLBACK'); + $this->Execute('SET AUTOCOMMIT=1'); + return $ok ? true : false; + } + + function RowLock($tables,$where='',$col='1 as adodbignore') + { + if ($this->transCnt==0) $this->BeginTrans(); + if ($where) $where = ' where '.$where; + $rs = $this->Execute("select $col from $tables $where for update"); + return !empty($rs); + } + +} + +class ADORecordSet_mysqlt extends ADORecordSet_mysql{ + var $databaseType = "mysqlt"; + + function __construct($queryID,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + + switch ($mode) + { + case ADODB_FETCH_NUM: $this->fetchMode = MYSQL_NUM; break; + case ADODB_FETCH_ASSOC:$this->fetchMode = MYSQL_ASSOC; break; + + case ADODB_FETCH_DEFAULT: + case ADODB_FETCH_BOTH: + default: $this->fetchMode = MYSQL_BOTH; break; + } + + $this->adodbFetchMode = $mode; + parent::__construct($queryID); + } + + function MoveNext() + { + if (@$this->fields = mysql_fetch_array($this->_queryID,$this->fetchMode)) { + $this->_currentRow += 1; + return true; + } + if (!$this->EOF) { + $this->_currentRow += 1; + $this->EOF = true; + } + return false; + } +} + +class ADORecordSet_ext_mysqlt extends ADORecordSet_mysqlt { + + function MoveNext() + { + return adodb_movenext($this); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-netezza.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-netezza.inc.php new file mode 100644 index 000000000..af3a1a11d --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-netezza.inc.php @@ -0,0 +1,157 @@ + 0 ORDER BY attnum"; + var $metaColumnsSQL1 = "SELECT attname, atttype FROM _v_relation_column_def WHERE name = '%s' AND attnum > 0 ORDER BY attnum"; + // netezza doesn't have keys. it does have distributions, so maybe this is + // something that can be pulled from the system tables + var $metaKeySQL = ""; + var $hasAffectedRows = true; + var $hasLimit = true; + var $true = 't'; // string that represents TRUE for a database + var $false = 'f'; // string that represents FALSE for a database + var $fmtDate = "'Y-m-d'"; // used by DBDate() as the default date format used by the database + var $fmtTimeStamp = "'Y-m-d G:i:s'"; // used by DBTimeStamp as the default timestamp fmt. + var $ansiOuter = true; + var $autoRollback = true; // apparently pgsql does not autorollback properly before 4.3.4 + // http://bugs.php.net/bug.php?id=25404 + + + function __construct() + { + + } + + function MetaColumns($table,$upper=true) + { + + // Changed this function to support Netezza which has no concept of keys + // could posisbly work on other things from the system table later. + + global $ADODB_FETCH_MODE; + + $table = strtolower($table); + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== false) $savem = $this->SetFetchMode(false); + + $rs = $this->Execute(sprintf($this->metaColumnsSQL,$table,$table)); + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + if ($rs === false) return false; + + $retarr = array(); + while (!$rs->EOF) { + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; + + // since we're returning type and length as one string, + // split them out here. + + if ($first = strstr($rs->fields[1], "(")) { + $fld->max_length = trim($first, "()"); + } else { + $fld->max_length = -1; + } + + if ($first = strpos($rs->fields[1], "(")) { + $fld->type = substr($rs->fields[1], 0, $first); + } else { + $fld->type = $rs->fields[1]; + } + + switch ($fld->type) { + case "byteint": + case "boolean": + $fld->max_length = 1; + break; + case "smallint": + $fld->max_length = 2; + break; + case "integer": + case "numeric": + case "date": + $fld->max_length = 4; + break; + case "bigint": + case "time": + case "timestamp": + $fld->max_length = 8; + break; + case "timetz": + case "time with time zone": + $fld->max_length = 12; + break; + } + + if ($ADODB_FETCH_MODE == ADODB_FETCH_NUM) $retarr[] = $fld; + else $retarr[($upper) ? strtoupper($fld->name) : $fld->name] = $fld; + + $rs->MoveNext(); + } + $rs->Close(); + return $retarr; + + } + + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordSet_netezza extends ADORecordSet_postgres64 +{ + var $databaseType = "netezza"; + var $canSeek = true; + + function __construct($queryID,$mode=false) + { + parent::__construct($queryID,$mode); + } + + // _initrs modified to disable blob handling + function _initrs() + { + global $ADODB_COUNTRECS; + $this->_numOfRows = ($ADODB_COUNTRECS)? @pg_num_rows($this->_queryID):-1; + $this->_numOfFields = @pg_num_fields($this->_queryID); + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-oci8.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-oci8.inc.php new file mode 100644 index 000000000..928d1b843 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-oci8.inc.php @@ -0,0 +1,1818 @@ + + + 13 Nov 2000 jlim - removed all ora_* references. +*/ + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +/* +NLS_Date_Format +Allows you to use a date format other than the Oracle Lite default. When a literal +character string appears where a date value is expected, the Oracle Lite database +tests the string to see if it matches the formats of Oracle, SQL-92, or the value +specified for this parameter in the POLITE.INI file. Setting this parameter also +defines the default format used in the TO_CHAR or TO_DATE functions when no +other format string is supplied. + +For Oracle the default is dd-mon-yy or dd-mon-yyyy, and for SQL-92 the default is +yy-mm-dd or yyyy-mm-dd. + +Using 'RR' in the format forces two-digit years less than or equal to 49 to be +interpreted as years in the 21st century (2000-2049), and years over 50 as years in +the 20th century (1950-1999). Setting the RR format as the default for all two-digit +year entries allows you to become year-2000 compliant. For example: +NLS_DATE_FORMAT='RR-MM-DD' + +You can also modify the date format using the ALTER SESSION command. +*/ + +# define the LOB descriptor type for the given type +# returns false if no LOB descriptor +function oci_lob_desc($type) { + switch ($type) { + case OCI_B_BFILE: return OCI_D_FILE; + case OCI_B_CFILEE: return OCI_D_FILE; + case OCI_B_CLOB: return OCI_D_LOB; + case OCI_B_BLOB: return OCI_D_LOB; + case OCI_B_ROWID: return OCI_D_ROWID; + } + return false; +} + +class ADODB_oci8 extends ADOConnection { + var $databaseType = 'oci8'; + var $dataProvider = 'oci8'; + var $replaceQuote = "''"; // string to use to replace quotes + var $concat_operator='||'; + var $sysDate = "TRUNC(SYSDATE)"; + var $sysTimeStamp = 'SYSDATE'; // requires oracle 9 or later, otherwise use SYSDATE + var $metaDatabasesSQL = "SELECT USERNAME FROM ALL_USERS WHERE USERNAME NOT IN ('SYS','SYSTEM','DBSNMP','OUTLN') ORDER BY 1"; + var $_stmt; + var $_commit = OCI_COMMIT_ON_SUCCESS; + var $_initdate = true; // init date to YYYY-MM-DD + var $metaTablesSQL = "select table_name,table_type from cat where table_type in ('TABLE','VIEW') and table_name not like 'BIN\$%'"; // bin$ tables are recycle bin tables + var $metaColumnsSQL = "select cname,coltype,width, SCALE, PRECISION, NULLS, DEFAULTVAL from col where tname='%s' order by colno"; //changed by smondino@users.sourceforge. net + var $metaColumnsSQL2 = "select column_name,data_type,data_length, data_scale, data_precision, + case when nullable = 'Y' then 'NULL' + else 'NOT NULL' end as nulls, + data_default from all_tab_cols + where owner='%s' and table_name='%s' order by column_id"; // when there is a schema + var $_bindInputArray = true; + var $hasGenID = true; + var $_genIDSQL = "SELECT (%s.nextval) FROM DUAL"; + var $_genSeqSQL = " +DECLARE + PRAGMA AUTONOMOUS_TRANSACTION; +BEGIN + execute immediate 'CREATE SEQUENCE %s START WITH %s'; +END; +"; + + var $_dropSeqSQL = "DROP SEQUENCE %s"; + var $hasAffectedRows = true; + var $random = "abs(mod(DBMS_RANDOM.RANDOM,10000001)/10000000)"; + var $noNullStrings = false; + var $connectSID = false; + var $_bind = false; + var $_nestedSQL = true; + var $_hasOciFetchStatement = false; + var $_getarray = false; // currently not working + var $leftOuter = ''; // oracle wierdness, $col = $value (+) for LEFT OUTER, $col (+)= $value for RIGHT OUTER + var $session_sharing_force_blob = false; // alter session on updateblob if set to true + var $firstrows = true; // enable first rows optimization on SelectLimit() + var $selectOffsetAlg1 = 1000; // when to use 1st algorithm of selectlimit. + var $NLS_DATE_FORMAT = 'YYYY-MM-DD'; // To include time, use 'RRRR-MM-DD HH24:MI:SS' + var $dateformat = 'YYYY-MM-DD'; // DBDate format + var $useDBDateFormatForTextInput=false; + var $datetime = false; // MetaType('DATE') returns 'D' (datetime==false) or 'T' (datetime == true) + var $_refLOBs = array(); + + // var $ansiOuter = true; // if oracle9 + + function __construct() + { + $this->_hasOciFetchStatement = ADODB_PHPVER >= 0x4200; + if (defined('ADODB_EXTENSION')) { + $this->rsPrefix .= 'ext_'; + } + } + + /* function MetaColumns($table, $normalize=true) added by smondino@users.sourceforge.net*/ + function MetaColumns($table, $normalize=true) + { + global $ADODB_FETCH_MODE; + + $schema = ''; + $this->_findschema($table, $schema); + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== false) { + $savem = $this->SetFetchMode(false); + } + + if ($schema){ + $rs = $this->Execute(sprintf($this->metaColumnsSQL2, strtoupper($schema), strtoupper($table))); + } + else { + $rs = $this->Execute(sprintf($this->metaColumnsSQL,strtoupper($table))); + } + + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + if (!$rs) { + return false; + } + $retarr = array(); + while (!$rs->EOF) { + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; + $fld->type = $rs->fields[1]; + $fld->max_length = $rs->fields[2]; + $fld->scale = $rs->fields[3]; + if ($rs->fields[1] == 'NUMBER') { + if ($rs->fields[3] == 0) { + $fld->type = 'INT'; + } + $fld->max_length = $rs->fields[4]; + } + $fld->not_null = (strncmp($rs->fields[5], 'NOT',3) === 0); + $fld->binary = (strpos($fld->type,'BLOB') !== false); + $fld->default_value = $rs->fields[6]; + + if ($ADODB_FETCH_MODE == ADODB_FETCH_NUM) { + $retarr[] = $fld; + } + else { + $retarr[strtoupper($fld->name)] = $fld; + } + $rs->MoveNext(); + } + $rs->Close(); + if (empty($retarr)) { + return false; + } + return $retarr; + } + + function Time() + { + $rs = $this->Execute("select TO_CHAR($this->sysTimeStamp,'YYYY-MM-DD HH24:MI:SS') from dual"); + if ($rs && !$rs->EOF) { + return $this->UnixTimeStamp(reset($rs->fields)); + } + + return false; + } + + /** + * Multiple modes of connection are supported: + * + * a. Local Database + * $conn->Connect(false,'scott','tiger'); + * + * b. From tnsnames.ora + * $conn->Connect($tnsname,'scott','tiger'); + * $conn->Connect(false,'scott','tiger',$tnsname); + * + * c. Server + service name + * $conn->Connect($serveraddress,'scott,'tiger',$service_name); + * + * d. Server + SID + * $conn->connectSID = true; + * $conn->Connect($serveraddress,'scott,'tiger',$SID); + * + * @param string|false $argHostname DB server hostname or TNS name + * @param string $argUsername + * @param string $argPassword + * @param string $argDatabasename Service name, SID (defaults to null) + * @param int $mode Connection mode, defaults to 0 + * (0 = non-persistent, 1 = persistent, 2 = force new connection) + * + * @return bool + */ + function _connect($argHostname, $argUsername, $argPassword, $argDatabasename=null, $mode=0) + { + if (!function_exists('oci_pconnect')) { + return null; + } + #adodb_backtrace(); + + $this->_errorMsg = false; + $this->_errorCode = false; + + if($argHostname) { // added by Jorma Tuomainen + if (empty($argDatabasename)) { + $argDatabasename = $argHostname; + } + else { + if(strpos($argHostname,":")) { + $argHostinfo=explode(":",$argHostname); + $argHostname=$argHostinfo[0]; + $argHostport=$argHostinfo[1]; + } else { + $argHostport = empty($this->port)? "1521" : $this->port; + } + + if (strncasecmp($argDatabasename,'SID=',4) == 0) { + $argDatabasename = substr($argDatabasename,4); + $this->connectSID = true; + } + + if ($this->connectSID) { + $argDatabasename="(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=".$argHostname + .")(PORT=$argHostport))(CONNECT_DATA=(SID=$argDatabasename)))"; + } else + $argDatabasename="(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=".$argHostname + .")(PORT=$argHostport))(CONNECT_DATA=(SERVICE_NAME=$argDatabasename)))"; + } + } + + //if ($argHostname) print "

Connect: 1st argument should be left blank for $this->databaseType

"; + if ($mode==1) { + $this->_connectionID = ($this->charSet) + ? oci_pconnect($argUsername,$argPassword, $argDatabasename,$this->charSet) + : oci_pconnect($argUsername,$argPassword, $argDatabasename); + if ($this->_connectionID && $this->autoRollback) { + oci_rollback($this->_connectionID); + } + } else if ($mode==2) { + $this->_connectionID = ($this->charSet) + ? oci_new_connect($argUsername,$argPassword, $argDatabasename,$this->charSet) + : oci_new_connect($argUsername,$argPassword, $argDatabasename); + } else { + $this->_connectionID = ($this->charSet) + ? oci_connect($argUsername,$argPassword, $argDatabasename,$this->charSet) + : oci_connect($argUsername,$argPassword, $argDatabasename); + } + if (!$this->_connectionID) { + return false; + } + + if ($this->_initdate) { + $this->Execute("ALTER SESSION SET NLS_DATE_FORMAT='".$this->NLS_DATE_FORMAT."'"); + } + + // looks like: + // Oracle8i Enterprise Edition Release 8.1.7.0.0 - Production With the Partitioning option JServer Release 8.1.7.0.0 - Production + // $vers = oci_server_version($this->_connectionID); + // if (strpos($vers,'8i') !== false) $this->ansiOuter = true; + return true; + } + + function ServerInfo() + { + $arr['compat'] = $this->GetOne('select value from sys.database_compatible_level'); + $arr['description'] = @oci_server_version($this->_connectionID); + $arr['version'] = ADOConnection::_findvers($arr['description']); + return $arr; + } + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabasename,1); + } + + // returns true or false + function _nconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabasename,2); + } + + function _affectedrows() + { + if (is_resource($this->_stmt)) { + return @oci_num_rows($this->_stmt); + } + return 0; + } + + function IfNull( $field, $ifNull ) + { + return " NVL($field, $ifNull) "; // if Oracle + } + + // format and return date string in database date format + function DBDate($d,$isfld=false) + { + if (empty($d) && $d !== 0) { + return 'null'; + } + + if ($isfld) { + $d = _adodb_safedate($d); + return 'TO_DATE('.$d.",'".$this->dateformat."')"; + } + + if (is_string($d)) { + $d = ADORecordSet::UnixDate($d); + } + + if (is_object($d)) { + $ds = $d->format($this->fmtDate); + } + else { + $ds = adodb_date($this->fmtDate,$d); + } + + return "TO_DATE(".$ds.",'".$this->dateformat."')"; + } + + function BindDate($d) + { + $d = ADOConnection::DBDate($d); + if (strncmp($d, "'", 1)) { + return $d; + } + + return substr($d, 1, strlen($d)-2); + } + + function BindTimeStamp($ts) + { + if (empty($ts) && $ts !== 0) { + return 'null'; + } + if (is_string($ts)) { + $ts = ADORecordSet::UnixTimeStamp($ts); + } + + if (is_object($ts)) { + $tss = $ts->format("'Y-m-d H:i:s'"); + } + else { + $tss = adodb_date("'Y-m-d H:i:s'",$ts); + } + + return $tss; + } + + // format and return date string in database timestamp format + function DBTimeStamp($ts,$isfld=false) + { + if (empty($ts) && $ts !== 0) { + return 'null'; + } + if ($isfld) { + return 'TO_DATE(substr('.$ts.",1,19),'RRRR-MM-DD, HH24:MI:SS')"; + } + if (is_string($ts)) { + $ts = ADORecordSet::UnixTimeStamp($ts); + } + + if (is_object($ts)) { + $tss = $ts->format("'Y-m-d H:i:s'"); + } + else { + $tss = date("'Y-m-d H:i:s'",$ts); + } + + return 'TO_DATE('.$tss.",'RRRR-MM-DD, HH24:MI:SS')"; + } + + function RowLock($tables,$where,$col='1 as adodbignore') + { + if ($this->autoCommit) { + $this->BeginTrans(); + } + return $this->GetOne("select $col from $tables where $where for update"); + } + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + if ($mask) { + $save = $this->metaTablesSQL; + $mask = $this->qstr(strtoupper($mask)); + $this->metaTablesSQL .= " AND upper(table_name) like $mask"; + } + $ret = ADOConnection::MetaTables($ttype,$showSchema); + + if ($mask) { + $this->metaTablesSQL = $save; + } + return $ret; + } + + // Mark Newnham + function MetaIndexes ($table, $primary = FALSE, $owner=false) + { + // save old fetch mode + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + + // get index details + $table = strtoupper($table); + + // get Primary index + $primary_key = ''; + + $rs = $this->Execute(sprintf("SELECT * FROM ALL_CONSTRAINTS WHERE UPPER(TABLE_NAME)='%s' AND CONSTRAINT_TYPE='P'",$table)); + if (!is_object($rs)) { + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + return false; + } + + if ($row = $rs->FetchRow()) { + $primary_key = $row[1]; //constraint_name + } + + if ($primary==TRUE && $primary_key=='') { + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + return false; //There is no primary key + } + + $rs = $this->Execute(sprintf("SELECT ALL_INDEXES.INDEX_NAME, ALL_INDEXES.UNIQUENESS, ALL_IND_COLUMNS.COLUMN_POSITION, ALL_IND_COLUMNS.COLUMN_NAME FROM ALL_INDEXES,ALL_IND_COLUMNS WHERE UPPER(ALL_INDEXES.TABLE_NAME)='%s' AND ALL_IND_COLUMNS.INDEX_NAME=ALL_INDEXES.INDEX_NAME",$table)); + + + if (!is_object($rs)) { + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + return false; + } + + $indexes = array (); + // parse index data into array + + while ($row = $rs->FetchRow()) { + if ($primary && $row[0] != $primary_key) { + continue; + } + if (!isset($indexes[$row[0]])) { + $indexes[$row[0]] = array( + 'unique' => ($row[1] == 'UNIQUE'), + 'columns' => array() + ); + } + $indexes[$row[0]]['columns'][$row[2] - 1] = $row[3]; + } + + // sort columns by order in the index + foreach ( array_keys ($indexes) as $index ) { + ksort ($indexes[$index]['columns']); + } + + if (isset($savem)) { + $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + } + return $indexes; + } + + function BeginTrans() + { + if ($this->transOff) { + return true; + } + $this->transCnt += 1; + $this->autoCommit = false; + $this->_commit = OCI_DEFAULT; + + if ($this->_transmode) { + $ok = $this->Execute("SET TRANSACTION ".$this->_transmode); + } + else { + $ok = true; + } + + return $ok ? true : false; + } + + function CommitTrans($ok=true) + { + if ($this->transOff) { + return true; + } + if (!$ok) { + return $this->RollbackTrans(); + } + + if ($this->transCnt) { + $this->transCnt -= 1; + } + $ret = oci_commit($this->_connectionID); + $this->_commit = OCI_COMMIT_ON_SUCCESS; + $this->autoCommit = true; + return $ret; + } + + function RollbackTrans() + { + if ($this->transOff) { + return true; + } + if ($this->transCnt) { + $this->transCnt -= 1; + } + $ret = oci_rollback($this->_connectionID); + $this->_commit = OCI_COMMIT_ON_SUCCESS; + $this->autoCommit = true; + return $ret; + } + + + function SelectDB($dbName) + { + return false; + } + + function ErrorMsg() + { + if ($this->_errorMsg !== false) { + return $this->_errorMsg; + } + + if (is_resource($this->_stmt)) { + $arr = @oci_error($this->_stmt); + } + if (empty($arr)) { + if (is_resource($this->_connectionID)) { + $arr = @oci_error($this->_connectionID); + } + else { + $arr = @oci_error(); + } + if ($arr === false) { + return ''; + } + } + $this->_errorMsg = $arr['message']; + $this->_errorCode = $arr['code']; + return $this->_errorMsg; + } + + function ErrorNo() + { + if ($this->_errorCode !== false) { + return $this->_errorCode; + } + + if (is_resource($this->_stmt)) { + $arr = @oci_error($this->_stmt); + } + if (empty($arr)) { + $arr = @oci_error($this->_connectionID); + if ($arr == false) { + $arr = @oci_error(); + } + if ($arr == false) { + return ''; + } + } + + $this->_errorMsg = $arr['message']; + $this->_errorCode = $arr['code']; + + return $arr['code']; + } + + /** + * Format date column in sql string given an input format that understands Y M D + */ + function SQLDate($fmt, $col=false) + { + if (!$col) { + $col = $this->sysTimeStamp; + } + $s = 'TO_CHAR('.$col.",'"; + + $len = strlen($fmt); + for ($i=0; $i < $len; $i++) { + $ch = $fmt[$i]; + switch($ch) { + case 'Y': + case 'y': + $s .= 'YYYY'; + break; + case 'Q': + case 'q': + $s .= 'Q'; + break; + + case 'M': + $s .= 'Mon'; + break; + + case 'm': + $s .= 'MM'; + break; + case 'D': + case 'd': + $s .= 'DD'; + break; + + case 'H': + $s.= 'HH24'; + break; + + case 'h': + $s .= 'HH'; + break; + + case 'i': + $s .= 'MI'; + break; + + case 's': + $s .= 'SS'; + break; + + case 'a': + case 'A': + $s .= 'AM'; + break; + + case 'w': + $s .= 'D'; + break; + + case 'l': + $s .= 'DAY'; + break; + + case 'W': + $s .= 'WW'; + break; + + default: + // handle escape characters... + if ($ch == '\\') { + $i++; + $ch = substr($fmt,$i,1); + } + if (strpos('-/.:;, ',$ch) !== false) { + $s .= $ch; + } + else { + $s .= '"'.$ch.'"'; + } + + } + } + return $s. "')"; + } + + function GetRandRow($sql, $arr = false) + { + $sql = "SELECT * FROM ($sql ORDER BY dbms_random.value) WHERE rownum = 1"; + + return $this->GetRow($sql,$arr); + } + + /** + * This algorithm makes use of + * + * a. FIRST_ROWS hint + * The FIRST_ROWS hint explicitly chooses the approach to optimize response + * time, that is, minimum resource usage to return the first row. Results + * will be returned as soon as they are identified. + * + * b. Uses rownum tricks to obtain only the required rows from a given offset. + * As this uses complicated sql statements, we only use this if $offset >= 100. + * This idea by Tomas V V Cox. + * + * This implementation does not appear to work with oracle 8.0.5 or earlier. + * Comment out this function then, and the slower SelectLimit() in the base + * class will be used. + * + * Note: FIRST_ROWS hinting is only used if $sql is a string; when + * processing a prepared statement's handle, no hinting is performed. + */ + function SelectLimit($sql,$nrows=-1,$offset=-1, $inputarr=false,$secs2cache=0) + { + // Since the methods used to limit the number of returned rows rely + // on modifying the provided SQL query, we can't work with prepared + // statements so we just extract the SQL string. + if(is_array($sql)) { + $sql = $sql[0]; + } + + // seems that oracle only supports 1 hint comment in 8i + if ($this->firstrows) { + if ($nrows > 500 && $nrows < 1000) { + $hint = "FIRST_ROWS($nrows)"; + } + else { + $hint = 'FIRST_ROWS'; + } + + if (strpos($sql,'/*+') !== false) { + $sql = str_replace('/*+ ',"/*+$hint ",$sql); + } + else { + $sql = preg_replace('/^[ \t\n]*select/i',"SELECT /*+$hint*/",$sql); + } + $hint = "/*+ $hint */"; + } else { + $hint = ''; + } + + if ($offset == -1 || ($offset < $this->selectOffsetAlg1 && 0 < $nrows && $nrows < 1000)) { + if ($nrows > 0) { + if ($offset > 0) { + $nrows += $offset; + } + $sql = "select * from (".$sql.") where rownum <= :adodb_offset"; + $inputarr['adodb_offset'] = $nrows; + $nrows = -1; + } + // note that $nrows = 0 still has to work ==> no rows returned + + return ADOConnection::SelectLimit($sql, $nrows, $offset, $inputarr, $secs2cache); + } else { + // Algorithm by Tomas V V Cox, from PEAR DB oci8.php + + // Let Oracle return the name of the columns + $q_fields = "SELECT * FROM (".$sql.") WHERE NULL = NULL"; + + if (! $stmt_arr = $this->Prepare($q_fields)) { + return false; + } + $stmt = $stmt_arr[1]; + + if (is_array($inputarr)) { + foreach($inputarr as $k => $v) { + $i=0; + if ($this->databaseType == 'oci8po') { + $bv_name = ":".$i++; + } else { + $bv_name = ":".$k; + } + if (is_array($v)) { + // suggested by g.giunta@libero. + if (sizeof($v) == 2) { + oci_bind_by_name($stmt,$bv_name,$inputarr[$k][0],$v[1]); + } + else { + oci_bind_by_name($stmt,$bv_name,$inputarr[$k][0],$v[1],$v[2]); + } + } else { + $len = -1; + if ($v === ' ') { + $len = 1; + } + if (isset($bindarr)) { // is prepared sql, so no need to oci_bind_by_name again + $bindarr[$k] = $v; + } else { // dynamic sql, so rebind every time + oci_bind_by_name($stmt,$bv_name,$inputarr[$k],$len); + } + } + } + } + + if (!oci_execute($stmt, OCI_DEFAULT)) { + oci_free_statement($stmt); + return false; + } + + $ncols = oci_num_fields($stmt); + for ( $i = 1; $i <= $ncols; $i++ ) { + $cols[] = '"'.oci_field_name($stmt, $i).'"'; + } + $result = false; + + oci_free_statement($stmt); + $fields = implode(',', $cols); + if ($nrows <= 0) { + $nrows = 999999999999; + } + else { + $nrows += $offset; + } + $offset += 1; // in Oracle rownum starts at 1 + + $sql = "SELECT $hint $fields FROM". + "(SELECT rownum as adodb_rownum, $fields FROM". + " ($sql) WHERE rownum <= :adodb_nrows". + ") WHERE adodb_rownum >= :adodb_offset"; + $inputarr['adodb_nrows'] = $nrows; + $inputarr['adodb_offset'] = $offset; + + if ($secs2cache > 0) { + $rs = $this->CacheExecute($secs2cache, $sql,$inputarr); + } + else { + $rs = $this->Execute($sql, $inputarr); + } + return $rs; + } + } + + /** + * Usage: + * Store BLOBs and CLOBs + * + * Example: to store $var in a blob + * $conn->Execute('insert into TABLE (id,ablob) values(12,empty_blob())'); + * $conn->UpdateBlob('TABLE', 'ablob', $varHoldingBlob, 'ID=12', 'BLOB'); + * + * $blobtype supports 'BLOB' and 'CLOB', but you need to change to 'empty_clob()'. + * + * to get length of LOB: + * select DBMS_LOB.GETLENGTH(ablob) from TABLE + * + * If you are using CURSOR_SHARING = force, it appears this will case a segfault + * under oracle 8.1.7.0. Run: + * $db->Execute('ALTER SESSION SET CURSOR_SHARING=EXACT'); + * before UpdateBlob() then... + */ + function UpdateBlob($table,$column,$val,$where,$blobtype='BLOB') + { + + //if (strlen($val) < 4000) return $this->Execute("UPDATE $table SET $column=:blob WHERE $where",array('blob'=>$val)) != false; + + switch(strtoupper($blobtype)) { + default: ADOConnection::outp("UpdateBlob: Unknown blobtype=$blobtype"); return false; + case 'BLOB': $type = OCI_B_BLOB; break; + case 'CLOB': $type = OCI_B_CLOB; break; + } + + if ($this->databaseType == 'oci8po') + $sql = "UPDATE $table set $column=EMPTY_{$blobtype}() WHERE $where RETURNING $column INTO ?"; + else + $sql = "UPDATE $table set $column=EMPTY_{$blobtype}() WHERE $where RETURNING $column INTO :blob"; + + $desc = oci_new_descriptor($this->_connectionID, OCI_D_LOB); + $arr['blob'] = array($desc,-1,$type); + if ($this->session_sharing_force_blob) { + $this->Execute('ALTER SESSION SET CURSOR_SHARING=EXACT'); + } + $commit = $this->autoCommit; + if ($commit) { + $this->BeginTrans(); + } + $rs = $this->_Execute($sql,$arr); + if ($rez = !empty($rs)) { + $desc->save($val); + } + $desc->free(); + if ($commit) { + $this->CommitTrans(); + } + if ($this->session_sharing_force_blob) { + $this->Execute('ALTER SESSION SET CURSOR_SHARING=FORCE'); + } + + if ($rez) { + $rs->Close(); + } + return $rez; + } + + /** + * Usage: store file pointed to by $val in a blob + */ + function UpdateBlobFile($table,$column,$val,$where,$blobtype='BLOB') + { + switch(strtoupper($blobtype)) { + default: ADOConnection::outp( "UpdateBlob: Unknown blobtype=$blobtype"); return false; + case 'BLOB': $type = OCI_B_BLOB; break; + case 'CLOB': $type = OCI_B_CLOB; break; + } + + if ($this->databaseType == 'oci8po') + $sql = "UPDATE $table set $column=EMPTY_{$blobtype}() WHERE $where RETURNING $column INTO ?"; + else + $sql = "UPDATE $table set $column=EMPTY_{$blobtype}() WHERE $where RETURNING $column INTO :blob"; + + $desc = oci_new_descriptor($this->_connectionID, OCI_D_LOB); + $arr['blob'] = array($desc,-1,$type); + + $this->BeginTrans(); + $rs = ADODB_oci8::Execute($sql,$arr); + if ($rez = !empty($rs)) { + $desc->savefile($val); + } + $desc->free(); + $this->CommitTrans(); + + if ($rez) { + $rs->Close(); + } + return $rez; + } + + /** + * Execute SQL + * + * @param sql SQL statement to execute, or possibly an array holding prepared statement ($sql[0] will hold sql text) + * @param [inputarr] holds the input data to bind to. Null elements will be set to null. + * @return RecordSet or false + */ + function Execute($sql,$inputarr=false) + { + if ($this->fnExecute) { + $fn = $this->fnExecute; + $ret = $fn($this,$sql,$inputarr); + if (isset($ret)) { + return $ret; + } + } + if ($inputarr !== false) { + if (!is_array($inputarr)) { + $inputarr = array($inputarr); + } + + $element0 = reset($inputarr); + $array2d = $this->bulkBind && is_array($element0) && !is_object(reset($element0)); + + # see http://phplens.com/lens/lensforum/msgs.php?id=18786 + if ($array2d || !$this->_bindInputArray) { + + # is_object check because oci8 descriptors can be passed in + if ($array2d && $this->_bindInputArray) { + if (is_string($sql)) { + $stmt = $this->Prepare($sql); + } else { + $stmt = $sql; + } + + foreach($inputarr as $arr) { + $ret = $this->_Execute($stmt,$arr); + if (!$ret) { + return $ret; + } + } + return $ret; + } else { + $sqlarr = explode(':', $sql); + $sql = ''; + $lastnomatch = -2; + #var_dump($sqlarr);echo "
";var_dump($inputarr);echo"
"; + foreach($sqlarr as $k => $str) { + if ($k == 0) { + $sql = $str; + continue; + } + // we need $lastnomatch because of the following datetime, + // eg. '10:10:01', which causes code to think that there is bind param :10 and :1 + $ok = preg_match('/^([0-9]*)/', $str, $arr); + + if (!$ok) { + $sql .= $str; + } else { + $at = $arr[1]; + if (isset($inputarr[$at]) || is_null($inputarr[$at])) { + if ((strlen($at) == strlen($str) && $k < sizeof($arr)-1)) { + $sql .= ':'.$str; + $lastnomatch = $k; + } else if ($lastnomatch == $k-1) { + $sql .= ':'.$str; + } else { + if (is_null($inputarr[$at])) { + $sql .= 'null'; + } + else { + $sql .= $this->qstr($inputarr[$at]); + } + $sql .= substr($str, strlen($at)); + } + } else { + $sql .= ':'.$str; + } + } + } + $inputarr = false; + } + } + $ret = $this->_Execute($sql,$inputarr); + + } else { + $ret = $this->_Execute($sql,false); + } + + return $ret; + } + + /* + * Example of usage: + * $stmt = $this->Prepare('insert into emp (empno, ename) values (:empno, :ename)'); + */ + function Prepare($sql,$cursor=false) + { + static $BINDNUM = 0; + + $stmt = oci_parse($this->_connectionID,$sql); + + if (!$stmt) { + $this->_errorMsg = false; + $this->_errorCode = false; + $arr = @oci_error($this->_connectionID); + if ($arr === false) { + return false; + } + + $this->_errorMsg = $arr['message']; + $this->_errorCode = $arr['code']; + return false; + } + + $BINDNUM += 1; + + $sttype = @oci_statement_type($stmt); + if ($sttype == 'BEGIN' || $sttype == 'DECLARE') { + return array($sql,$stmt,0,$BINDNUM, ($cursor) ? oci_new_cursor($this->_connectionID) : false); + } + return array($sql,$stmt,0,$BINDNUM); + } + + /* + Call an oracle stored procedure and returns a cursor variable as a recordset. + Concept by Robert Tuttle robert@ud.com + + Example: + Note: we return a cursor variable in :RS2 + $rs = $db->ExecuteCursor("BEGIN adodb.open_tab(:RS2); END;",'RS2'); + + $rs = $db->ExecuteCursor( + "BEGIN :RS2 = adodb.getdata(:VAR1); END;", + 'RS2', + array('VAR1' => 'Mr Bean')); + + */ + function ExecuteCursor($sql,$cursorName='rs',$params=false) + { + if (is_array($sql)) { + $stmt = $sql; + } + else $stmt = ADODB_oci8::Prepare($sql,true); # true to allocate oci_new_cursor + + if (is_array($stmt) && sizeof($stmt) >= 5) { + $hasref = true; + $ignoreCur = false; + $this->Parameter($stmt, $ignoreCur, $cursorName, false, -1, OCI_B_CURSOR); + if ($params) { + foreach($params as $k => $v) { + $this->Parameter($stmt,$params[$k], $k); + } + } + } else + $hasref = false; + + $rs = $this->Execute($stmt); + if ($rs) { + if ($rs->databaseType == 'array') { + oci_free_cursor($stmt[4]); + } + elseif ($hasref) { + $rs->_refcursor = $stmt[4]; + } + } + return $rs; + } + + /** + * Bind a variable -- very, very fast for executing repeated statements in oracle. + * + * Better than using + * for ($i = 0; $i < $max; $i++) { + * $p1 = ?; $p2 = ?; $p3 = ?; + * $this->Execute("insert into table (col0, col1, col2) values (:0, :1, :2)", array($p1,$p2,$p3)); + * } + * + * Usage: + * $stmt = $DB->Prepare("insert into table (col0, col1, col2) values (:0, :1, :2)"); + * $DB->Bind($stmt, $p1); + * $DB->Bind($stmt, $p2); + * $DB->Bind($stmt, $p3); + * for ($i = 0; $i < $max; $i++) { + * $p1 = ?; $p2 = ?; $p3 = ?; + * $DB->Execute($stmt); + * } + * + * Some timings to insert 1000 records, test table has 3 cols, and 1 index. + * - Time 0.6081s (1644.60 inserts/sec) with direct oci_parse/oci_execute + * - Time 0.6341s (1577.16 inserts/sec) with ADOdb Prepare/Bind/Execute + * - Time 1.5533s ( 643.77 inserts/sec) with pure SQL using Execute + * + * Now if PHP only had batch/bulk updating like Java or PL/SQL... + * + * Note that the order of parameters differs from oci_bind_by_name, + * because we default the names to :0, :1, :2 + */ + function Bind(&$stmt,&$var,$size=4000,$type=false,$name=false,$isOutput=false) + { + + if (!is_array($stmt)) { + return false; + } + + if (($type == OCI_B_CURSOR) && sizeof($stmt) >= 5) { + return oci_bind_by_name($stmt[1],":".$name,$stmt[4],$size,$type); + } + + if ($name == false) { + if ($type !== false) { + $rez = oci_bind_by_name($stmt[1],":".$stmt[2],$var,$size,$type); + } + else { + $rez = oci_bind_by_name($stmt[1],":".$stmt[2],$var,$size); // +1 byte for null terminator + } + $stmt[2] += 1; + } else if (oci_lob_desc($type)) { + if ($this->debug) { + ADOConnection::outp("Bind: name = $name"); + } + //we have to create a new Descriptor here + $numlob = count($this->_refLOBs); + $this->_refLOBs[$numlob]['LOB'] = oci_new_descriptor($this->_connectionID, oci_lob_desc($type)); + $this->_refLOBs[$numlob]['TYPE'] = $isOutput; + + $tmp = $this->_refLOBs[$numlob]['LOB']; + $rez = oci_bind_by_name($stmt[1], ":".$name, $tmp, -1, $type); + if ($this->debug) { + ADOConnection::outp("Bind: descriptor has been allocated, var (".$name.") binded"); + } + + // if type is input then write data to lob now + if ($isOutput == false) { + $var = $this->BlobEncode($var); + $tmp->WriteTemporary($var); + $this->_refLOBs[$numlob]['VAR'] = &$var; + if ($this->debug) { + ADOConnection::outp("Bind: LOB has been written to temp"); + } + } else { + $this->_refLOBs[$numlob]['VAR'] = &$var; + } + $rez = $tmp; + } else { + if ($this->debug) + ADOConnection::outp("Bind: name = $name"); + + if ($type !== false) { + $rez = oci_bind_by_name($stmt[1],":".$name,$var,$size,$type); + } + else { + $rez = oci_bind_by_name($stmt[1],":".$name,$var,$size); // +1 byte for null terminator + } + } + + return $rez; + } + + function Param($name,$type='C') + { + return ':'.$name; + } + + /** + * Usage: + * $stmt = $db->Prepare('select * from table where id =:myid and group=:group'); + * $db->Parameter($stmt,$id,'myid'); + * $db->Parameter($stmt,$group,'group'); + * $db->Execute($stmt); + * + * @param $stmt Statement returned by Prepare() or PrepareSP(). + * @param $var PHP variable to bind to + * @param $name Name of stored procedure variable name to bind to. + * @param [$isOutput] Indicates direction of parameter 0/false=IN 1=OUT 2= IN/OUT. This is ignored in oci8. + * @param [$maxLen] Holds an maximum length of the variable. + * @param [$type] The data type of $var. Legal values depend on driver. + * + * @link http://php.net/oci_bind_by_name + */ + function Parameter(&$stmt,&$var,$name,$isOutput=false,$maxLen=4000,$type=false) + { + if ($this->debug) { + $prefix = ($isOutput) ? 'Out' : 'In'; + $ztype = (empty($type)) ? 'false' : $type; + ADOConnection::outp( "{$prefix}Parameter(\$stmt, \$php_var='$var', \$name='$name', \$maxLen=$maxLen, \$type=$ztype);"); + } + return $this->Bind($stmt,$var,$maxLen,$type,$name,$isOutput); + } + + /** + * returns query ID if successful, otherwise false + * this version supports: + * + * 1. $db->execute('select * from table'); + * + * 2. $db->prepare('insert into table (a,b,c) values (:0,:1,:2)'); + * $db->execute($prepared_statement, array(1,2,3)); + * + * 3. $db->execute('insert into table (a,b,c) values (:a,:b,:c)',array('a'=>1,'b'=>2,'c'=>3)); + * + * 4. $db->prepare('insert into table (a,b,c) values (:0,:1,:2)'); + * $db->bind($stmt,1); $db->bind($stmt,2); $db->bind($stmt,3); + * $db->execute($stmt); + */ + function _query($sql,$inputarr=false) + { + if (is_array($sql)) { // is prepared sql + $stmt = $sql[1]; + + // we try to bind to permanent array, so that oci_bind_by_name is persistent + // and carried out once only - note that max array element size is 4000 chars + if (is_array($inputarr)) { + $bindpos = $sql[3]; + if (isset($this->_bind[$bindpos])) { + // all tied up already + $bindarr = $this->_bind[$bindpos]; + } else { + // one statement to bind them all + $bindarr = array(); + foreach($inputarr as $k => $v) { + $bindarr[$k] = $v; + oci_bind_by_name($stmt,":$k",$bindarr[$k],is_string($v) && strlen($v)>4000 ? -1 : 4000); + } + $this->_bind[$bindpos] = $bindarr; + } + } + } else { + $stmt=oci_parse($this->_connectionID,$sql); + } + + $this->_stmt = $stmt; + if (!$stmt) { + return false; + } + + if (defined('ADODB_PREFETCH_ROWS')) { + @oci_set_prefetch($stmt,ADODB_PREFETCH_ROWS); + } + + if (is_array($inputarr)) { + foreach($inputarr as $k => $v) { + if (is_array($v)) { + // suggested by g.giunta@libero. + if (sizeof($v) == 2) { + oci_bind_by_name($stmt,":$k",$inputarr[$k][0],$v[1]); + } + else { + oci_bind_by_name($stmt,":$k",$inputarr[$k][0],$v[1],$v[2]); + } + + if ($this->debug==99) { + if (is_object($v[0])) { + echo "name=:$k",' len='.$v[1],' type='.$v[2],'
'; + } + else { + echo "name=:$k",' var='.$inputarr[$k][0],' len='.$v[1],' type='.$v[2],'
'; + } + + } + } else { + $len = -1; + if ($v === ' ') { + $len = 1; + } + if (isset($bindarr)) { // is prepared sql, so no need to oci_bind_by_name again + $bindarr[$k] = $v; + } else { // dynamic sql, so rebind every time + oci_bind_by_name($stmt,":$k",$inputarr[$k],$len); + } + } + } + } + + $this->_errorMsg = false; + $this->_errorCode = false; + if (oci_execute($stmt,$this->_commit)) { + + if (count($this -> _refLOBs) > 0) { + + foreach ($this -> _refLOBs as $key => $value) { + if ($this -> _refLOBs[$key]['TYPE'] == true) { + $tmp = $this -> _refLOBs[$key]['LOB'] -> load(); + if ($this -> debug) { + ADOConnection::outp("OUT LOB: LOB has been loaded.
"); + } + //$_GLOBALS[$this -> _refLOBs[$key]['VAR']] = $tmp; + $this -> _refLOBs[$key]['VAR'] = $tmp; + } else { + $this->_refLOBs[$key]['LOB']->save($this->_refLOBs[$key]['VAR']); + $this -> _refLOBs[$key]['LOB']->free(); + unset($this -> _refLOBs[$key]); + if ($this->debug) { + ADOConnection::outp("IN LOB: LOB has been saved.
"); + } + } + } + } + + switch (@oci_statement_type($stmt)) { + case "SELECT": + return $stmt; + + case 'DECLARE': + case "BEGIN": + if (is_array($sql) && !empty($sql[4])) { + $cursor = $sql[4]; + if (is_resource($cursor)) { + $ok = oci_execute($cursor); + return $cursor; + } + return $stmt; + } else { + if (is_resource($stmt)) { + oci_free_statement($stmt); + return true; + } + return $stmt; + } + break; + default : + + return true; + } + } + return false; + } + + // From Oracle Whitepaper: PHP Scalability and High Availability + function IsConnectionError($err) + { + switch($err) { + case 378: /* buffer pool param incorrect */ + case 602: /* core dump */ + case 603: /* fatal error */ + case 609: /* attach failed */ + case 1012: /* not logged in */ + case 1033: /* init or shutdown in progress */ + case 1043: /* Oracle not available */ + case 1089: /* immediate shutdown in progress */ + case 1090: /* shutdown in progress */ + case 1092: /* instance terminated */ + case 3113: /* disconnect */ + case 3114: /* not connected */ + case 3122: /* closing window */ + case 3135: /* lost contact */ + case 12153: /* TNS: not connected */ + case 27146: /* fatal or instance terminated */ + case 28511: /* Lost RPC */ + return true; + } + return false; + } + + // returns true or false + function _close() + { + if (!$this->_connectionID) { + return; + } + + + if (!$this->autoCommit) { + oci_rollback($this->_connectionID); + } + if (count($this->_refLOBs) > 0) { + foreach ($this ->_refLOBs as $key => $value) { + $this->_refLOBs[$key]['LOB']->free(); + unset($this->_refLOBs[$key]); + } + } + oci_close($this->_connectionID); + + $this->_stmt = false; + $this->_connectionID = false; + } + + function MetaPrimaryKeys($table, $owner=false,$internalKey=false) + { + if ($internalKey) { + return array('ROWID'); + } + + // tested with oracle 8.1.7 + $table = strtoupper($table); + if ($owner) { + $owner_clause = "AND ((a.OWNER = b.OWNER) AND (a.OWNER = UPPER('$owner')))"; + $ptab = 'ALL_'; + } else { + $owner_clause = ''; + $ptab = 'USER_'; + } + $sql = " +SELECT /*+ RULE */ distinct b.column_name + FROM {$ptab}CONSTRAINTS a + , {$ptab}CONS_COLUMNS b + WHERE ( UPPER(b.table_name) = ('$table')) + AND (UPPER(a.table_name) = ('$table') and a.constraint_type = 'P') + $owner_clause + AND (a.constraint_name = b.constraint_name)"; + + $rs = $this->Execute($sql); + if ($rs && !$rs->EOF) { + $arr = $rs->GetArray(); + $a = array(); + foreach($arr as $v) { + $a[] = reset($v); + } + return $a; + } + else return false; + } + + /** + * returns assoc array where keys are tables, and values are foreign keys + * + * @param str $table + * @param str $owner [optional][default=NULL] + * @param bool $upper [optional][discarded] + * @return mixed[] Array of foreign key information + * + * @link http://gis.mit.edu/classes/11.521/sqlnotes/referential_integrity.html + */ + function MetaForeignKeys($table, $owner=false, $upper=false) + { + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $table = $this->qstr(strtoupper($table)); + if (!$owner) { + $owner = $this->user; + $tabp = 'user_'; + } else + $tabp = 'all_'; + + $owner = ' and owner='.$this->qstr(strtoupper($owner)); + + $sql = +"select constraint_name,r_owner,r_constraint_name + from {$tabp}constraints + where constraint_type = 'R' and table_name = $table $owner"; + + $constraints = $this->GetArray($sql); + $arr = false; + foreach($constraints as $constr) { + $cons = $this->qstr($constr[0]); + $rowner = $this->qstr($constr[1]); + $rcons = $this->qstr($constr[2]); + $cols = $this->GetArray("select column_name from {$tabp}cons_columns where constraint_name=$cons $owner order by position"); + $tabcol = $this->GetArray("select table_name,column_name from {$tabp}cons_columns where owner=$rowner and constraint_name=$rcons order by position"); + + if ($cols && $tabcol) + for ($i=0, $max=sizeof($cols); $i < $max; $i++) { + $arr[$tabcol[$i][0]] = $cols[$i][0].'='.$tabcol[$i][1]; + } + } + $ADODB_FETCH_MODE = $save; + + return $arr; + } + + + function CharMax() + { + return 4000; + } + + function TextMax() + { + return 4000; + } + + /** + * Quotes a string. + * An example is $db->qstr("Don't bother",magic_quotes_runtime()); + * + * @param string $s the string to quote + * @param bool $magic_quotes if $s is GET/POST var, set to get_magic_quotes_gpc(). + * This undoes the stupidity of magic quotes for GPC. + * + * @return string quoted string to be sent back to database + */ + function qstr($s,$magic_quotes=false) + { + //$nofixquotes=false; + + if ($this->noNullStrings && strlen($s)==0) { + $s = ' '; + } + if (!$magic_quotes) { + if ($this->replaceQuote[0] == '\\'){ + $s = str_replace('\\','\\\\',$s); + } + return "'".str_replace("'",$this->replaceQuote,$s)."'"; + } + + // undo magic quotes for " unless sybase is on + if (!ini_get('magic_quotes_sybase')) { + $s = str_replace('\\"','"',$s); + $s = str_replace('\\\\','\\',$s); + return "'".str_replace("\\'",$this->replaceQuote,$s)."'"; + } else { + return "'".$s."'"; + } + } + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordset_oci8 extends ADORecordSet { + + var $databaseType = 'oci8'; + var $bind=false; + var $_fieldobjs; + + function __construct($queryID,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + switch ($mode) { + case ADODB_FETCH_ASSOC: + $this->fetchMode = OCI_ASSOC; + break; + case ADODB_FETCH_DEFAULT: + case ADODB_FETCH_BOTH: + $this->fetchMode = OCI_NUM + OCI_ASSOC; + break; + case ADODB_FETCH_NUM: + default: + $this->fetchMode = OCI_NUM; + break; + } + $this->fetchMode += OCI_RETURN_NULLS + OCI_RETURN_LOBS; + $this->adodbFetchMode = $mode; + $this->_queryID = $queryID; + } + + + function Init() + { + if ($this->_inited) { + return; + } + + $this->_inited = true; + if ($this->_queryID) { + + $this->_currentRow = 0; + @$this->_initrs(); + if ($this->_numOfFields) { + $this->EOF = !$this->_fetch(); + } + else $this->EOF = true; + + /* + // based on idea by Gaetano Giunta to detect unusual oracle errors + // see http://phplens.com/lens/lensforum/msgs.php?id=6771 + $err = oci_error($this->_queryID); + if ($err && $this->connection->debug) { + ADOConnection::outp($err); + } + */ + + if (!is_array($this->fields)) { + $this->_numOfRows = 0; + $this->fields = array(); + } + } else { + $this->fields = array(); + $this->_numOfRows = 0; + $this->_numOfFields = 0; + $this->EOF = true; + } + } + + function _initrs() + { + $this->_numOfRows = -1; + $this->_numOfFields = oci_num_fields($this->_queryID); + if ($this->_numOfFields>0) { + $this->_fieldobjs = array(); + $max = $this->_numOfFields; + for ($i=0;$i<$max; $i++) $this->_fieldobjs[] = $this->_FetchField($i); + } + } + + /** + * Get column information in the Recordset object. + * fetchField() can be used in order to obtain information about fields + * in a certain query result. If the field offset isn't specified, the next + * field that wasn't yet retrieved by fetchField() is retrieved + * + * @return object containing field information + */ + function _FetchField($fieldOffset = -1) + { + $fld = new ADOFieldObject; + $fieldOffset += 1; + $fld->name =oci_field_name($this->_queryID, $fieldOffset); + if (ADODB_ASSOC_CASE == ADODB_ASSOC_CASE_LOWER) { + $fld->name = strtolower($fld->name); + } + $fld->type = oci_field_type($this->_queryID, $fieldOffset); + $fld->max_length = oci_field_size($this->_queryID, $fieldOffset); + + switch($fld->type) { + case 'NUMBER': + $p = oci_field_precision($this->_queryID, $fieldOffset); + $sc = oci_field_scale($this->_queryID, $fieldOffset); + if ($p != 0 && $sc == 0) { + $fld->type = 'INT'; + } + $fld->scale = $p; + break; + + case 'CLOB': + case 'NCLOB': + case 'BLOB': + $fld->max_length = -1; + break; + } + return $fld; + } + + /* For some reason, oci_field_name fails when called after _initrs() so we cache it */ + function FetchField($fieldOffset = -1) + { + return $this->_fieldobjs[$fieldOffset]; + } + + + function MoveNext() + { + if ($this->fields = @oci_fetch_array($this->_queryID,$this->fetchMode)) { + $this->_currentRow += 1; + $this->_updatefields(); + return true; + } + if (!$this->EOF) { + $this->_currentRow += 1; + $this->EOF = true; + } + return false; + } + + // Optimize SelectLimit() by using oci_fetch() + function GetArrayLimit($nrows,$offset=-1) + { + if ($offset <= 0) { + $arr = $this->GetArray($nrows); + return $arr; + } + $arr = array(); + for ($i=1; $i < $offset; $i++) { + if (!@oci_fetch($this->_queryID)) { + return $arr; + } + } + + if (!$this->fields = @oci_fetch_array($this->_queryID,$this->fetchMode)) { + return $arr; + } + $this->_updatefields(); + $results = array(); + $cnt = 0; + while (!$this->EOF && $nrows != $cnt) { + $results[$cnt++] = $this->fields; + $this->MoveNext(); + } + + return $results; + } + + + // Use associative array to get fields array + function Fields($colname) + { + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + + function _seek($row) + { + return false; + } + + function _fetch() + { + $this->fields = @oci_fetch_array($this->_queryID,$this->fetchMode); + $this->_updatefields(); + + return $this->fields; + } + + /** + * close() only needs to be called if you are worried about using too much + * memory while your script is running. All associated result memory for the + * specified result identifier will automatically be freed. + */ + function _close() + { + if ($this->connection->_stmt === $this->_queryID) { + $this->connection->_stmt = false; + } + if (!empty($this->_refcursor)) { + oci_free_cursor($this->_refcursor); + $this->_refcursor = false; + } + @oci_free_statement($this->_queryID); + $this->_queryID = false; + } + + /** + * not the fastest implementation - quick and dirty - jlim + * for best performance, use the actual $rs->MetaType(). + * + * @param mixed $t + * @param int $len [optional] Length of blobsize + * @param bool $fieldobj [optional][discarded] + * @return str The metatype of the field + */ + function MetaType($t, $len=-1, $fieldobj=false) + { + if (is_object($t)) { + $fieldobj = $t; + $t = $fieldobj->type; + $len = $fieldobj->max_length; + } + + switch (strtoupper($t)) { + case 'VARCHAR': + case 'VARCHAR2': + case 'CHAR': + case 'VARBINARY': + case 'BINARY': + case 'NCHAR': + case 'NVARCHAR': + case 'NVARCHAR2': + if ($len <= $this->blobSize) { + return 'C'; + } + + case 'NCLOB': + case 'LONG': + case 'LONG VARCHAR': + case 'CLOB': + return 'X'; + + case 'LONG RAW': + case 'LONG VARBINARY': + case 'BLOB': + return 'B'; + + case 'DATE': + return ($this->connection->datetime) ? 'T' : 'D'; + + + case 'TIMESTAMP': return 'T'; + + case 'INT': + case 'SMALLINT': + case 'INTEGER': + return 'I'; + + default: + return 'N'; + } + } +} + +class ADORecordSet_ext_oci8 extends ADORecordSet_oci8 { + function __construct($queryID,$mode=false) + { + parent::__construct($queryID, $mode); + } + + function MoveNext() + { + return adodb_movenext($this); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-oci805.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-oci805.inc.php new file mode 100644 index 000000000..112e9eccd --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-oci805.inc.php @@ -0,0 +1,55 @@ + 0) { + if ($offset > 0) $nrows += $offset; + $sql = "select * from ($sql) where rownum <= $nrows"; + $nrows = -1; + } + */ + + return ADOConnection::SelectLimit($sql,$nrows,$offset,$inputarr,$secs2cache); + } +} + +class ADORecordset_oci805 extends ADORecordset_oci8 { + var $databaseType = "oci805"; + function __construct($id,$mode=false) + { + parent::__construct($id,$mode); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-oci8po.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-oci8po.inc.php new file mode 100644 index 000000000..6b939b0a0 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-oci8po.inc.php @@ -0,0 +1,227 @@ + + + Should some emulation of RecordCount() be implemented? + +*/ + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +include_once(ADODB_DIR.'/drivers/adodb-oci8.inc.php'); + +class ADODB_oci8po extends ADODB_oci8 { + var $databaseType = 'oci8po'; + var $dataProvider = 'oci8'; + var $metaColumnsSQL = "select lower(cname),coltype,width, SCALE, PRECISION, NULLS, DEFAULTVAL from col where tname='%s' order by colno"; //changed by smondino@users.sourceforge. net + var $metaTablesSQL = "select lower(table_name),table_type from cat where table_type in ('TABLE','VIEW')"; + + function __construct() + { + $this->_hasOCIFetchStatement = ADODB_PHPVER >= 0x4200; + # oci8po does not support adodb extension: adodb_movenext() + } + + function Param($name,$type='C') + { + return '?'; + } + + function Prepare($sql,$cursor=false) + { + $sqlarr = explode('?',$sql); + $sql = $sqlarr[0]; + for ($i = 1, $max = sizeof($sqlarr); $i < $max; $i++) { + $sql .= ':'.($i-1) . $sqlarr[$i]; + } + return ADODB_oci8::Prepare($sql,$cursor); + } + + function Execute($sql,$inputarr=false) + { + return ADOConnection::Execute($sql,$inputarr); + } + + /** + * The optimizations performed by ADODB_oci8::SelectLimit() are not + * compatible with the oci8po driver, so we rely on the slower method + * from the base class. + * We can't properly handle prepared statements either due to preprocessing + * of query parameters, so we treat them as regular SQL statements. + */ + function SelectLimit($sql, $nrows=-1, $offset=-1, $inputarr=false, $secs2cache=0) + { + if(is_array($sql)) { +// $sql = $sql[0]; + } + return ADOConnection::SelectLimit($sql, $nrows, $offset, $inputarr, $secs2cache); + } + + // emulate handling of parameters ? ?, replacing with :bind0 :bind1 + function _query($sql,$inputarr=false) + { + if (is_array($inputarr)) { + $i = 0; + if (is_array($sql)) { + foreach($inputarr as $v) { + $arr['bind'.$i++] = $v; + } + } else { + // Need to identify if the ? is inside a quoted string, and if + // so not use it as a bind variable + preg_match_all('/".*\??"|\'.*\?.*?\'/', $sql, $matches); + foreach($matches[0] as $qmMatch){ + $qmReplace = str_replace('?', '-QUESTIONMARK-', $qmMatch); + $sql = str_replace($qmMatch, $qmReplace, $sql); + } + + // Replace parameters if any were found + $sqlarr = explode('?',$sql); + if(count($sqlarr) > 1) { + $sql = $sqlarr[0]; + + foreach ($inputarr as $k => $v) { + $sql .= ":$k" . $sqlarr[++$i]; + } + } + + $sql = str_replace('-QUESTIONMARK-', '?', $sql); + } + } + return ADODB_oci8::_query($sql,$inputarr); + } +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordset_oci8po extends ADORecordset_oci8 { + + var $databaseType = 'oci8po'; + + function __construct($queryID,$mode=false) + { + parent::__construct($queryID,$mode); + } + + function Fields($colname) + { + if ($this->fetchMode & OCI_ASSOC) return $this->fields[$colname]; + + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + // lowercase field names... + function _FetchField($fieldOffset = -1) + { + $fld = new ADOFieldObject; + $fieldOffset += 1; + $fld->name = OCIcolumnname($this->_queryID, $fieldOffset); + if (ADODB_ASSOC_CASE == ADODB_ASSOC_CASE_LOWER) { + $fld->name = strtolower($fld->name); + } + $fld->type = OCIcolumntype($this->_queryID, $fieldOffset); + $fld->max_length = OCIcolumnsize($this->_queryID, $fieldOffset); + if ($fld->type == 'NUMBER') { + $sc = OCIColumnScale($this->_queryID, $fieldOffset); + if ($sc == 0) { + $fld->type = 'INT'; + } + } + return $fld; + } + + // 10% speedup to move MoveNext to child class + function MoveNext() + { + $ret = @oci_fetch_array($this->_queryID,$this->fetchMode); + if($ret !== false) { + global $ADODB_ANSI_PADDING_OFF; + $this->fields = $ret; + $this->_currentRow++; + $this->_updatefields(); + + if (!empty($ADODB_ANSI_PADDING_OFF)) { + foreach($this->fields as $k => $v) { + if (is_string($v)) $this->fields[$k] = rtrim($v); + } + } + return true; + } + if (!$this->EOF) { + $this->EOF = true; + $this->_currentRow++; + } + return false; + } + + /* Optimize SelectLimit() by using OCIFetch() instead of OCIFetchInto() */ + function GetArrayLimit($nrows,$offset=-1) + { + if ($offset <= 0) { + $arr = $this->GetArray($nrows); + return $arr; + } + for ($i=1; $i < $offset; $i++) + if (!@OCIFetch($this->_queryID)) { + $arr = array(); + return $arr; + } + $ret = @oci_fetch_array($this->_queryID,$this->fetchMode); + if ($ret === false) { + $arr = array(); + return $arr; + } + $this->fields = $ret; + $this->_updatefields(); + $results = array(); + $cnt = 0; + while (!$this->EOF && $nrows != $cnt) { + $results[$cnt++] = $this->fields; + $this->MoveNext(); + } + + return $results; + } + + function _fetch() + { + global $ADODB_ANSI_PADDING_OFF; + + $ret = @oci_fetch_array($this->_queryID,$this->fetchMode); + if ($ret) { + $this->fields = $ret; + $this->_updatefields(); + + if (!empty($ADODB_ANSI_PADDING_OFF)) { + foreach($this->fields as $k => $v) { + if (is_string($v)) $this->fields[$k] = rtrim($v); + } + } + } + return $ret !== false; + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-oci8quercus.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-oci8quercus.inc.php new file mode 100644 index 000000000..1940e802e --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-oci8quercus.inc.php @@ -0,0 +1,89 @@ + + + Should some emulation of RecordCount() be implemented? + +*/ + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +include_once(ADODB_DIR.'/drivers/adodb-oci8.inc.php'); + +class ADODB_oci8quercus extends ADODB_oci8 { + var $databaseType = 'oci8quercus'; + var $dataProvider = 'oci8'; + + function __construct() + { + } + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordset_oci8quercus extends ADORecordset_oci8 { + + var $databaseType = 'oci8quercus'; + + function __construct($queryID,$mode=false) + { + parent::__construct($queryID,$mode); + } + + function _FetchField($fieldOffset = -1) + { + global $QUERCUS; + $fld = new ADOFieldObject; + + if (!empty($QUERCUS)) { + $fld->name = oci_field_name($this->_queryID, $fieldOffset); + $fld->type = oci_field_type($this->_queryID, $fieldOffset); + $fld->max_length = oci_field_size($this->_queryID, $fieldOffset); + + //if ($fld->name == 'VAL6_NUM_12_4') $fld->type = 'NUMBER'; + switch($fld->type) { + case 'string': $fld->type = 'VARCHAR'; break; + case 'real': $fld->type = 'NUMBER'; break; + } + } else { + $fieldOffset += 1; + $fld->name = oci_field_name($this->_queryID, $fieldOffset); + $fld->type = oci_field_type($this->_queryID, $fieldOffset); + $fld->max_length = oci_field_size($this->_queryID, $fieldOffset); + } + switch($fld->type) { + case 'NUMBER': + $p = oci_field_precision($this->_queryID, $fieldOffset); + $sc = oci_field_scale($this->_queryID, $fieldOffset); + if ($p != 0 && $sc == 0) $fld->type = 'INT'; + $fld->scale = $p; + break; + + case 'CLOB': + case 'NCLOB': + case 'BLOB': + $fld->max_length = -1; + break; + } + + return $fld; + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-odbc.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-odbc.inc.php new file mode 100644 index 000000000..efaa5bb1a --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-odbc.inc.php @@ -0,0 +1,738 @@ +_haserrorfunctions = ADODB_PHPVER >= 0x4050; + $this->_has_stupid_odbc_fetch_api_change = ADODB_PHPVER >= 0x4200; + } + + // returns true or false + function _connect($argDSN, $argUsername, $argPassword, $argDatabasename) + { + global $php_errormsg; + + if (!function_exists('odbc_connect')) return null; + + if (!empty($argDatabasename) && stristr($argDSN, 'Database=') === false) { + $argDSN = trim($argDSN); + $endDSN = substr($argDSN, strlen($argDSN) - 1); + if ($endDSN != ';') $argDSN .= ';'; + $argDSN .= 'Database='.$argDatabasename; + } + + if (isset($php_errormsg)) $php_errormsg = ''; + if ($this->curmode === false) $this->_connectionID = odbc_connect($argDSN,$argUsername,$argPassword); + else $this->_connectionID = odbc_connect($argDSN,$argUsername,$argPassword,$this->curmode); + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + if (isset($this->connectStmt)) $this->Execute($this->connectStmt); + + return $this->_connectionID != false; + } + + // returns true or false + function _pconnect($argDSN, $argUsername, $argPassword, $argDatabasename) + { + global $php_errormsg; + + if (!function_exists('odbc_connect')) return null; + + if (isset($php_errormsg)) $php_errormsg = ''; + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + if ($this->debug && $argDatabasename) { + ADOConnection::outp("For odbc PConnect(), $argDatabasename is not used. Place dsn in 1st parameter."); + } + // print "dsn=$argDSN u=$argUsername p=$argPassword
"; flush(); + if ($this->curmode === false) $this->_connectionID = odbc_connect($argDSN,$argUsername,$argPassword); + else $this->_connectionID = odbc_pconnect($argDSN,$argUsername,$argPassword,$this->curmode); + + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + if ($this->_connectionID && $this->autoRollback) @odbc_rollback($this->_connectionID); + if (isset($this->connectStmt)) $this->Execute($this->connectStmt); + + return $this->_connectionID != false; + } + + + function ServerInfo() + { + + if (!empty($this->host) && ADODB_PHPVER >= 0x4300) { + $dsn = strtoupper($this->host); + $first = true; + $found = false; + + if (!function_exists('odbc_data_source')) return false; + + while(true) { + + $rez = @odbc_data_source($this->_connectionID, + $first ? SQL_FETCH_FIRST : SQL_FETCH_NEXT); + $first = false; + if (!is_array($rez)) break; + if (strtoupper($rez['server']) == $dsn) { + $found = true; + break; + } + } + if (!$found) return ADOConnection::ServerInfo(); + if (!isset($rez['version'])) $rez['version'] = ''; + return $rez; + } else { + return ADOConnection::ServerInfo(); + } + } + + + function CreateSequence($seqname='adodbseq',$start=1) + { + if (empty($this->_genSeqSQL)) return false; + $ok = $this->Execute(sprintf($this->_genSeqSQL,$seqname)); + if (!$ok) return false; + $start -= 1; + return $this->Execute("insert into $seqname values($start)"); + } + + var $_dropSeqSQL = 'drop table %s'; + function DropSequence($seqname = 'adodbseq') + { + if (empty($this->_dropSeqSQL)) return false; + return $this->Execute(sprintf($this->_dropSeqSQL,$seqname)); + } + + /* + This algorithm is not very efficient, but works even if table locking + is not available. + + Will return false if unable to generate an ID after $MAXLOOPS attempts. + */ + function GenID($seq='adodbseq',$start=1) + { + // if you have to modify the parameter below, your database is overloaded, + // or you need to implement generation of id's yourself! + $MAXLOOPS = 100; + //$this->debug=1; + while (--$MAXLOOPS>=0) { + $num = $this->GetOne("select id from $seq"); + if ($num === false) { + $this->Execute(sprintf($this->_genSeqSQL ,$seq)); + $start -= 1; + $num = '0'; + $ok = $this->Execute("insert into $seq values($start)"); + if (!$ok) return false; + } + $this->Execute("update $seq set id=id+1 where id=$num"); + + if ($this->affected_rows() > 0) { + $num += 1; + $this->genID = $num; + return $num; + } elseif ($this->affected_rows() == 0) { + // some drivers do not return a valid value => try with another method + $value = $this->GetOne("select id from $seq"); + if ($value == $num + 1) { + return $value; + } + } + } + if ($fn = $this->raiseErrorFn) { + $fn($this->databaseType,'GENID',-32000,"Unable to generate unique id after $MAXLOOPS attempts",$seq,$num); + } + return false; + } + + + function ErrorMsg() + { + if ($this->_haserrorfunctions) { + if ($this->_errorMsg !== false) return $this->_errorMsg; + if (empty($this->_connectionID)) return @odbc_errormsg(); + return @odbc_errormsg($this->_connectionID); + } else return ADOConnection::ErrorMsg(); + } + + function ErrorNo() + { + + if ($this->_haserrorfunctions) { + if ($this->_errorCode !== false) { + // bug in 4.0.6, error number can be corrupted string (should be 6 digits) + return (strlen($this->_errorCode)<=2) ? 0 : $this->_errorCode; + } + + if (empty($this->_connectionID)) $e = @odbc_error(); + else $e = @odbc_error($this->_connectionID); + + // bug in 4.0.6, error number can be corrupted string (should be 6 digits) + // so we check and patch + if (strlen($e)<=2) return 0; + return $e; + } else return ADOConnection::ErrorNo(); + } + + + + function BeginTrans() + { + if (!$this->hasTransactions) return false; + if ($this->transOff) return true; + $this->transCnt += 1; + $this->_autocommit = false; + return odbc_autocommit($this->_connectionID,false); + } + + function CommitTrans($ok=true) + { + if ($this->transOff) return true; + if (!$ok) return $this->RollbackTrans(); + if ($this->transCnt) $this->transCnt -= 1; + $this->_autocommit = true; + $ret = odbc_commit($this->_connectionID); + odbc_autocommit($this->_connectionID,true); + return $ret; + } + + function RollbackTrans() + { + if ($this->transOff) return true; + if ($this->transCnt) $this->transCnt -= 1; + $this->_autocommit = true; + $ret = odbc_rollback($this->_connectionID); + odbc_autocommit($this->_connectionID,true); + return $ret; + } + + function MetaPrimaryKeys($table,$owner=false) + { + global $ADODB_FETCH_MODE; + + if ($this->uCaseTables) $table = strtoupper($table); + $schema = ''; + $this->_findschema($table,$schema); + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $qid = @odbc_primarykeys($this->_connectionID,'',$schema,$table); + + if (!$qid) { + $ADODB_FETCH_MODE = $savem; + return false; + } + $rs = new ADORecordSet_odbc($qid); + $ADODB_FETCH_MODE = $savem; + + if (!$rs) return false; + $rs->_has_stupid_odbc_fetch_api_change = $this->_has_stupid_odbc_fetch_api_change; + + $arr = $rs->GetArray(); + $rs->Close(); + //print_r($arr); + $arr2 = array(); + for ($i=0; $i < sizeof($arr); $i++) { + if ($arr[$i][3]) $arr2[] = $arr[$i][3]; + } + return $arr2; + } + + + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + global $ADODB_FETCH_MODE; + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $qid = odbc_tables($this->_connectionID); + + $rs = new ADORecordSet_odbc($qid); + + $ADODB_FETCH_MODE = $savem; + if (!$rs) { + $false = false; + return $false; + } + $rs->_has_stupid_odbc_fetch_api_change = $this->_has_stupid_odbc_fetch_api_change; + + $arr = $rs->GetArray(); + //print_r($arr); + + $rs->Close(); + $arr2 = array(); + + if ($ttype) { + $isview = strncmp($ttype,'V',1) === 0; + } + for ($i=0; $i < sizeof($arr); $i++) { + if (!$arr[$i][2]) continue; + $type = $arr[$i][3]; + if ($ttype) { + if ($isview) { + if (strncmp($type,'V',1) === 0) $arr2[] = $arr[$i][2]; + } else if (strncmp($type,'SYS',3) !== 0) $arr2[] = $arr[$i][2]; + } else if (strncmp($type,'SYS',3) !== 0) $arr2[] = $arr[$i][2]; + } + return $arr2; + } + +/* +See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/odbc/htm/odbcdatetime_data_type_changes.asp +/ SQL data type codes / +#define SQL_UNKNOWN_TYPE 0 +#define SQL_CHAR 1 +#define SQL_NUMERIC 2 +#define SQL_DECIMAL 3 +#define SQL_INTEGER 4 +#define SQL_SMALLINT 5 +#define SQL_FLOAT 6 +#define SQL_REAL 7 +#define SQL_DOUBLE 8 +#if (ODBCVER >= 0x0300) +#define SQL_DATETIME 9 +#endif +#define SQL_VARCHAR 12 + + +/ One-parameter shortcuts for date/time data types / +#if (ODBCVER >= 0x0300) +#define SQL_TYPE_DATE 91 +#define SQL_TYPE_TIME 92 +#define SQL_TYPE_TIMESTAMP 93 + +#define SQL_UNICODE (-95) +#define SQL_UNICODE_VARCHAR (-96) +#define SQL_UNICODE_LONGVARCHAR (-97) +*/ + function ODBCTypes($t) + { + switch ((integer)$t) { + case 1: + case 12: + case 0: + case -95: + case -96: + return 'C'; + case -97: + case -1: //text + return 'X'; + case -4: //image + return 'B'; + + case 9: + case 91: + return 'D'; + + case 10: + case 11: + case 92: + case 93: + return 'T'; + + case 4: + case 5: + case -6: + return 'I'; + + case -11: // uniqidentifier + return 'R'; + case -7: //bit + return 'L'; + + default: + return 'N'; + } + } + + function MetaColumns($table, $normalize=true) + { + global $ADODB_FETCH_MODE; + + $false = false; + if ($this->uCaseTables) $table = strtoupper($table); + $schema = ''; + $this->_findschema($table,$schema); + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + /*if (false) { // after testing, confirmed that the following does not work becoz of a bug + $qid2 = odbc_tables($this->_connectionID); + $rs = new ADORecordSet_odbc($qid2); + $ADODB_FETCH_MODE = $savem; + if (!$rs) return false; + $rs->_has_stupid_odbc_fetch_api_change = $this->_has_stupid_odbc_fetch_api_change; + $rs->_fetch(); + + while (!$rs->EOF) { + if ($table == strtoupper($rs->fields[2])) { + $q = $rs->fields[0]; + $o = $rs->fields[1]; + break; + } + $rs->MoveNext(); + } + $rs->Close(); + + $qid = odbc_columns($this->_connectionID,$q,$o,strtoupper($table),'%'); + } */ + + switch ($this->databaseType) { + case 'access': + case 'vfp': + $qid = odbc_columns($this->_connectionID);#,'%','',strtoupper($table),'%'); + break; + + + case 'db2': + $colname = "%"; + $qid = odbc_columns($this->_connectionID, "", $schema, $table, $colname); + break; + + default: + $qid = @odbc_columns($this->_connectionID,'%','%',strtoupper($table),'%'); + if (empty($qid)) $qid = odbc_columns($this->_connectionID); + break; + } + if (empty($qid)) return $false; + + $rs = new ADORecordSet_odbc($qid); + $ADODB_FETCH_MODE = $savem; + + if (!$rs) return $false; + $rs->_has_stupid_odbc_fetch_api_change = $this->_has_stupid_odbc_fetch_api_change; + $rs->_fetch(); + + $retarr = array(); + + /* + $rs->fields indices + 0 TABLE_QUALIFIER + 1 TABLE_SCHEM + 2 TABLE_NAME + 3 COLUMN_NAME + 4 DATA_TYPE + 5 TYPE_NAME + 6 PRECISION + 7 LENGTH + 8 SCALE + 9 RADIX + 10 NULLABLE + 11 REMARKS + */ + while (!$rs->EOF) { + // adodb_pr($rs->fields); + if (strtoupper(trim($rs->fields[2])) == $table && (!$schema || strtoupper($rs->fields[1]) == $schema)) { + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[3]; + $fld->type = $this->ODBCTypes($rs->fields[4]); + + // ref: http://msdn.microsoft.com/library/default.asp?url=/archive/en-us/dnaraccgen/html/msdn_odk.asp + // access uses precision to store length for char/varchar + if ($fld->type == 'C' or $fld->type == 'X') { + if ($this->databaseType == 'access') + $fld->max_length = $rs->fields[6]; + else if ($rs->fields[4] <= -95) // UNICODE + $fld->max_length = $rs->fields[7]/2; + else + $fld->max_length = $rs->fields[7]; + } else + $fld->max_length = $rs->fields[7]; + $fld->not_null = !empty($rs->fields[10]); + $fld->scale = $rs->fields[8]; + $retarr[strtoupper($fld->name)] = $fld; + } else if (sizeof($retarr)>0) + break; + $rs->MoveNext(); + } + $rs->Close(); //-- crashes 4.03pl1 -- why? + + if (empty($retarr)) $retarr = false; + return $retarr; + } + + function Prepare($sql) + { + if (! $this->_bindInputArray) return $sql; // no binding + $stmt = odbc_prepare($this->_connectionID,$sql); + if (!$stmt) { + // we don't know whether odbc driver is parsing prepared stmts, so just return sql + return $sql; + } + return array($sql,$stmt,false); + } + + /* returns queryID or false */ + function _query($sql,$inputarr=false) + { + GLOBAL $php_errormsg; + if (isset($php_errormsg)) $php_errormsg = ''; + $this->_error = ''; + + if ($inputarr) { + if (is_array($sql)) { + $stmtid = $sql[1]; + } else { + $stmtid = odbc_prepare($this->_connectionID,$sql); + + if ($stmtid == false) { + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + return false; + } + } + + if (! odbc_execute($stmtid,$inputarr)) { + //@odbc_free_result($stmtid); + if ($this->_haserrorfunctions) { + $this->_errorMsg = odbc_errormsg(); + $this->_errorCode = odbc_error(); + } + return false; + } + + } else if (is_array($sql)) { + $stmtid = $sql[1]; + if (!odbc_execute($stmtid)) { + //@odbc_free_result($stmtid); + if ($this->_haserrorfunctions) { + $this->_errorMsg = odbc_errormsg(); + $this->_errorCode = odbc_error(); + } + return false; + } + } else + $stmtid = odbc_exec($this->_connectionID,$sql); + + $this->_lastAffectedRows = 0; + if ($stmtid) { + if (@odbc_num_fields($stmtid) == 0) { + $this->_lastAffectedRows = odbc_num_rows($stmtid); + $stmtid = true; + } else { + $this->_lastAffectedRows = 0; + odbc_binmode($stmtid,$this->binmode); + odbc_longreadlen($stmtid,$this->maxblobsize); + } + + if ($this->_haserrorfunctions) { + $this->_errorMsg = ''; + $this->_errorCode = 0; + } else + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + } else { + if ($this->_haserrorfunctions) { + $this->_errorMsg = odbc_errormsg(); + $this->_errorCode = odbc_error(); + } else + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : ''; + } + return $stmtid; + } + + /* + Insert a null into the blob field of the table first. + Then use UpdateBlob to store the blob. + + Usage: + + $conn->Execute('INSERT INTO blobtable (id, blobcol) VALUES (1, null)'); + $conn->UpdateBlob('blobtable','blobcol',$blob,'id=1'); + */ + function UpdateBlob($table,$column,$val,$where,$blobtype='BLOB') + { + return $this->Execute("UPDATE $table SET $column=? WHERE $where",array($val)) != false; + } + + // returns true or false + function _close() + { + $ret = @odbc_close($this->_connectionID); + $this->_connectionID = false; + return $ret; + } + + function _affectedrows() + { + return $this->_lastAffectedRows; + } + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordSet_odbc extends ADORecordSet { + + var $bind = false; + var $databaseType = "odbc"; + var $dataProvider = "odbc"; + var $useFetchArray; + var $_has_stupid_odbc_fetch_api_change; + + function __construct($id,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + $this->fetchMode = $mode; + + $this->_queryID = $id; + + // the following is required for mysql odbc driver in 4.3.1 -- why? + $this->EOF = false; + $this->_currentRow = -1; + //parent::__construct($id); + } + + + // returns the field object + function FetchField($fieldOffset = -1) + { + + $off=$fieldOffset+1; // offsets begin at 1 + + $o= new ADOFieldObject(); + $o->name = @odbc_field_name($this->_queryID,$off); + $o->type = @odbc_field_type($this->_queryID,$off); + $o->max_length = @odbc_field_len($this->_queryID,$off); + if (ADODB_ASSOC_CASE == 0) $o->name = strtolower($o->name); + else if (ADODB_ASSOC_CASE == 1) $o->name = strtoupper($o->name); + return $o; + } + + /* Use associative array to get fields array */ + function Fields($colname) + { + if ($this->fetchMode & ADODB_FETCH_ASSOC) return $this->fields[$colname]; + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + + function _initrs() + { + global $ADODB_COUNTRECS; + $this->_numOfRows = ($ADODB_COUNTRECS) ? @odbc_num_rows($this->_queryID) : -1; + $this->_numOfFields = @odbc_num_fields($this->_queryID); + // some silly drivers such as db2 as/400 and intersystems cache return _numOfRows = 0 + if ($this->_numOfRows == 0) $this->_numOfRows = -1; + //$this->useFetchArray = $this->connection->useFetchArray; + $this->_has_stupid_odbc_fetch_api_change = ADODB_PHPVER >= 0x4200; + } + + function _seek($row) + { + return false; + } + + // speed up SelectLimit() by switching to ADODB_FETCH_NUM as ADODB_FETCH_ASSOC is emulated + function GetArrayLimit($nrows,$offset=-1) + { + if ($offset <= 0) { + $rs = $this->GetArray($nrows); + return $rs; + } + $savem = $this->fetchMode; + $this->fetchMode = ADODB_FETCH_NUM; + $this->Move($offset); + $this->fetchMode = $savem; + + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + $this->fields = $this->GetRowAssoc(); + } + + $results = array(); + $cnt = 0; + while (!$this->EOF && $nrows != $cnt) { + $results[$cnt++] = $this->fields; + $this->MoveNext(); + } + + return $results; + } + + + function MoveNext() + { + if ($this->_numOfRows != 0 && !$this->EOF) { + $this->_currentRow++; + if( $this->_fetch() ) { + return true; + } + } + $this->fields = false; + $this->EOF = true; + return false; + } + + function _fetch() + { + $this->fields = false; + if ($this->_has_stupid_odbc_fetch_api_change) + $rez = @odbc_fetch_into($this->_queryID,$this->fields); + else { + $row = 0; + $rez = @odbc_fetch_into($this->_queryID,$row,$this->fields); + } + if ($rez) { + if ($this->fetchMode & ADODB_FETCH_ASSOC) { + $this->fields = $this->GetRowAssoc(); + } + return true; + } + return false; + } + + function _close() + { + return @odbc_free_result($this->_queryID); + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-odbc_db2.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-odbc_db2.inc.php new file mode 100644 index 000000000..fa6d8b849 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-odbc_db2.inc.php @@ -0,0 +1,369 @@ +curMode = SQL_CUR_USE_ODBC; +$db->Connect($dsn, $userid, $pwd); + + + +USING CLI INTERFACE +=================== + +I have had reports that the $host and $database params have to be reversed in +Connect() when using the CLI interface. From Halmai Csongor csongor.halmai#nexum.hu: + +> The symptom is that if I change the database engine from postgres or any other to DB2 then the following +> connection command becomes wrong despite being described this version to be correct in the docs. +> +> $connection_object->Connect( $DATABASE_HOST, $DATABASE_AUTH_USER_NAME, $DATABASE_AUTH_PASSWORD, $DATABASE_NAME ) +> +> In case of DB2 I had to swap the first and last arguments in order to connect properly. + + +System Error 5 +============== +IF you get a System Error 5 when trying to Connect/Load, it could be a permission problem. Give the user connecting +to DB2 full rights to the DB2 SQLLIB directory, and place the user in the DBUSERS group. +*/ + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +if (!defined('_ADODB_ODBC_LAYER')) { + include(ADODB_DIR."/drivers/adodb-odbc.inc.php"); +} +if (!defined('ADODB_ODBC_DB2')){ +define('ADODB_ODBC_DB2',1); + +class ADODB_ODBC_DB2 extends ADODB_odbc { + var $databaseType = "db2"; + var $concat_operator = '||'; + var $sysTime = 'CURRENT TIME'; + var $sysDate = 'CURRENT DATE'; + var $sysTimeStamp = 'CURRENT TIMESTAMP'; + // The complete string representation of a timestamp has the form + // yyyy-mm-dd-hh.mm.ss.nnnnnn. + var $fmtTimeStamp = "'Y-m-d-H.i.s'"; + var $ansiOuter = true; + var $identitySQL = 'values IDENTITY_VAL_LOCAL()'; + var $_bindInputArray = true; + var $hasInsertID = true; + var $rsPrefix = 'ADORecordset_odbc_'; + + function __construct() + { + if (strncmp(PHP_OS,'WIN',3) === 0) $this->curmode = SQL_CUR_USE_ODBC; + parent::__construct(); + } + + function IfNull( $field, $ifNull ) + { + return " COALESCE($field, $ifNull) "; // if DB2 UDB + } + + function ServerInfo() + { + //odbc_setoption($this->_connectionID,1,101 /*SQL_ATTR_ACCESS_MODE*/, 1 /*SQL_MODE_READ_ONLY*/); + $vers = $this->GetOne('select versionnumber from sysibm.sysversions'); + //odbc_setoption($this->_connectionID,1,101, 0 /*SQL_MODE_READ_WRITE*/); + return array('description'=>'DB2 ODBC driver', 'version'=>$vers); + } + + function _insertid() + { + return $this->GetOne($this->identitySQL); + } + + function RowLock($tables,$where,$col='1 as adodbignore') + { + if ($this->_autocommit) $this->BeginTrans(); + return $this->GetOne("select $col from $tables where $where for update"); + } + + function MetaTables($ttype=false,$showSchema=false, $qtable="%", $qschema="%") + { + global $ADODB_FETCH_MODE; + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $qid = odbc_tables($this->_connectionID, "", $qschema, $qtable, ""); + + $rs = new ADORecordSet_odbc($qid); + + $ADODB_FETCH_MODE = $savem; + if (!$rs) { + $false = false; + return $false; + } + $rs->_has_stupid_odbc_fetch_api_change = $this->_has_stupid_odbc_fetch_api_change; + + $arr = $rs->GetArray(); + //print_r($arr); + + $rs->Close(); + $arr2 = array(); + + if ($ttype) { + $isview = strncmp($ttype,'V',1) === 0; + } + for ($i=0; $i < sizeof($arr); $i++) { + + if (!$arr[$i][2]) continue; + if (strncmp($arr[$i][1],'SYS',3) === 0) continue; + + $type = $arr[$i][3]; + + if ($showSchema) $arr[$i][2] = $arr[$i][1].'.'.$arr[$i][2]; + + if ($ttype) { + if ($isview) { + if (strncmp($type,'V',1) === 0) $arr2[] = $arr[$i][2]; + } else if (strncmp($type,'T',1) === 0) $arr2[] = $arr[$i][2]; + } else if (strncmp($type,'S',1) !== 0) $arr2[] = $arr[$i][2]; + } + return $arr2; + } + + function MetaIndexes ($table, $primary = FALSE, $owner=false) + { + // save old fetch mode + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + $false = false; + // get index details + $table = strtoupper($table); + $SQL="SELECT NAME, UNIQUERULE, COLNAMES FROM SYSIBM.SYSINDEXES WHERE TBNAME='$table'"; + if ($primary) + $SQL.= " AND UNIQUERULE='P'"; + $rs = $this->Execute($SQL); + if (!is_object($rs)) { + if (isset($savem)) + $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + return $false; + } + $indexes = array (); + // parse index data into array + while ($row = $rs->FetchRow()) { + $indexes[$row[0]] = array( + 'unique' => ($row[1] == 'U' || $row[1] == 'P'), + 'columns' => array() + ); + $cols = ltrim($row[2],'+'); + $indexes[$row[0]]['columns'] = explode('+', $cols); + } + if (isset($savem)) { + $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + } + return $indexes; + } + + // Format date column in sql string given an input format that understands Y M D + function SQLDate($fmt, $col=false) + { + // use right() and replace() ? + if (!$col) $col = $this->sysDate; + $s = ''; + + $len = strlen($fmt); + for ($i=0; $i < $len; $i++) { + if ($s) $s .= '||'; + $ch = $fmt[$i]; + switch($ch) { + case 'Y': + case 'y': + $s .= "char(year($col))"; + break; + case 'M': + $s .= "substr(monthname($col),1,3)"; + break; + case 'm': + $s .= "right(digits(month($col)),2)"; + break; + case 'D': + case 'd': + $s .= "right(digits(day($col)),2)"; + break; + case 'H': + case 'h': + if ($col != $this->sysDate) $s .= "right(digits(hour($col)),2)"; + else $s .= "''"; + break; + case 'i': + case 'I': + if ($col != $this->sysDate) + $s .= "right(digits(minute($col)),2)"; + else $s .= "''"; + break; + case 'S': + case 's': + if ($col != $this->sysDate) + $s .= "right(digits(second($col)),2)"; + else $s .= "''"; + break; + default: + if ($ch == '\\') { + $i++; + $ch = substr($fmt,$i,1); + } + $s .= $this->qstr($ch); + } + } + return $s; + } + + + function SelectLimit($sql, $nrows = -1, $offset = -1, $inputArr = false, $secs2cache = 0) + { + $nrows = (integer) $nrows; + if ($offset <= 0) { + // could also use " OPTIMIZE FOR $nrows ROWS " + if ($nrows >= 0) $sql .= " FETCH FIRST $nrows ROWS ONLY "; + $rs = $this->Execute($sql,$inputArr); + } else { + if ($offset > 0 && $nrows < 0); + else { + $nrows += $offset; + $sql .= " FETCH FIRST $nrows ROWS ONLY "; + } + $rs = ADOConnection::SelectLimit($sql,-1,$offset,$inputArr); + } + + return $rs; + } + +}; + + +class ADORecordSet_odbc_db2 extends ADORecordSet_odbc { + + var $databaseType = "db2"; + + function __construct($id,$mode=false) + { + parent::__construct($id,$mode); + } + + function MetaType($t,$len=-1,$fieldobj=false) + { + if (is_object($t)) { + $fieldobj = $t; + $t = $fieldobj->type; + $len = $fieldobj->max_length; + } + + switch (strtoupper($t)) { + case 'VARCHAR': + case 'CHAR': + case 'CHARACTER': + case 'C': + if ($len <= $this->blobSize) return 'C'; + + case 'LONGCHAR': + case 'TEXT': + case 'CLOB': + case 'DBCLOB': // double-byte + case 'X': + return 'X'; + + case 'BLOB': + case 'GRAPHIC': + case 'VARGRAPHIC': + return 'B'; + + case 'DATE': + case 'D': + return 'D'; + + case 'TIME': + case 'TIMESTAMP': + case 'T': + return 'T'; + + //case 'BOOLEAN': + //case 'BIT': + // return 'L'; + + //case 'COUNTER': + // return 'R'; + + case 'INT': + case 'INTEGER': + case 'BIGINT': + case 'SMALLINT': + case 'I': + return 'I'; + + default: return 'N'; + } + } +} + +} //define diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-odbc_mssql.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-odbc_mssql.inc.php new file mode 100644 index 000000000..aa6431680 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-odbc_mssql.inc.php @@ -0,0 +1,363 @@ + 'master'"; + var $metaTablesSQL="select name,case when type='U' then 'T' else 'V' end from sysobjects where (type='U' or type='V') and (name not in ('sysallocations','syscolumns','syscomments','sysdepends','sysfilegroups','sysfiles','sysfiles1','sysforeignkeys','sysfulltextcatalogs','sysindexes','sysindexkeys','sysmembers','sysobjects','syspermissions','sysprotects','sysreferences','systypes','sysusers','sysalternates','sysconstraints','syssegments','REFERENTIAL_CONSTRAINTS','CHECK_CONSTRAINTS','CONSTRAINT_TABLE_USAGE','CONSTRAINT_COLUMN_USAGE','VIEWS','VIEW_TABLE_USAGE','VIEW_COLUMN_USAGE','SCHEMATA','TABLES','TABLE_CONSTRAINTS','TABLE_PRIVILEGES','COLUMNS','COLUMN_DOMAIN_USAGE','COLUMN_PRIVILEGES','DOMAINS','DOMAIN_CONSTRAINTS','KEY_COLUMN_USAGE'))"; + var $metaColumnsSQL = # xtype==61 is datetime + "select c.name,t.name,c.length,c.isnullable, c.status, + (case when c.xusertype=61 then 0 else c.xprec end), + (case when c.xusertype=61 then 0 else c.xscale end) + from syscolumns c join systypes t on t.xusertype=c.xusertype join sysobjects o on o.id=c.id where o.name='%s'"; + var $hasTop = 'top'; // support mssql/interbase SELECT TOP 10 * FROM TABLE + var $sysDate = 'GetDate()'; + var $sysTimeStamp = 'GetDate()'; + var $leftOuter = '*='; + var $rightOuter = '=*'; + var $substr = 'substring'; + var $length = 'len'; + var $ansiOuter = true; // for mssql7 or later + var $identitySQL = 'select SCOPE_IDENTITY()'; // 'select SCOPE_IDENTITY'; # for mssql 2000 + var $hasInsertID = true; + var $connectStmt = 'SET CONCAT_NULL_YIELDS_NULL OFF'; # When SET CONCAT_NULL_YIELDS_NULL is ON, + # concatenating a null value with a string yields a NULL result + + function __construct() + { + parent::__construct(); + //$this->curmode = SQL_CUR_USE_ODBC; + } + + // crashes php... + function ServerInfo() + { + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $row = $this->GetRow("execute sp_server_info 2"); + $ADODB_FETCH_MODE = $save; + if (!is_array($row)) return false; + $arr['description'] = $row[2]; + $arr['version'] = ADOConnection::_findvers($arr['description']); + return $arr; + } + + function IfNull( $field, $ifNull ) + { + return " ISNULL($field, $ifNull) "; // if MS SQL Server + } + + function _insertid() + { + // SCOPE_IDENTITY() + // Returns the last IDENTITY value inserted into an IDENTITY column in + // the same scope. A scope is a module -- a stored procedure, trigger, + // function, or batch. Thus, two statements are in the same scope if + // they are in the same stored procedure, function, or batch. + return $this->GetOne($this->identitySQL); + } + + + function MetaForeignKeys($table, $owner=false, $upper=false) + { + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $table = $this->qstr(strtoupper($table)); + + $sql = +"select object_name(constid) as constraint_name, + col_name(fkeyid, fkey) as column_name, + object_name(rkeyid) as referenced_table_name, + col_name(rkeyid, rkey) as referenced_column_name +from sysforeignkeys +where upper(object_name(fkeyid)) = $table +order by constraint_name, referenced_table_name, keyno"; + + $constraints = $this->GetArray($sql); + + $ADODB_FETCH_MODE = $save; + + $arr = false; + foreach($constraints as $constr) { + //print_r($constr); + $arr[$constr[0]][$constr[2]][] = $constr[1].'='.$constr[3]; + } + if (!$arr) return false; + + $arr2 = false; + + foreach($arr as $k => $v) { + foreach($v as $a => $b) { + if ($upper) $a = strtoupper($a); + $arr2[$a] = $b; + } + } + return $arr2; + } + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + if ($mask) {//$this->debug=1; + $save = $this->metaTablesSQL; + $mask = $this->qstr($mask); + $this->metaTablesSQL .= " AND name like $mask"; + } + $ret = ADOConnection::MetaTables($ttype,$showSchema); + + if ($mask) { + $this->metaTablesSQL = $save; + } + return $ret; + } + + function MetaColumns($table, $normalize=true) + { + + $this->_findschema($table,$schema); + if ($schema) { + $dbName = $this->database; + $this->SelectDB($schema); + } + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + if ($this->fetchMode !== false) $savem = $this->SetFetchMode(false); + $rs = $this->Execute(sprintf($this->metaColumnsSQL,$table)); + + if ($schema) { + $this->SelectDB($dbName); + } + + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + if (!is_object($rs)) { + $false = false; + return $false; + } + + $retarr = array(); + while (!$rs->EOF){ + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; + $fld->type = $rs->fields[1]; + + $fld->not_null = (!$rs->fields[3]); + $fld->auto_increment = ($rs->fields[4] == 128); // sys.syscolumns status field. 0x80 = 128 ref: http://msdn.microsoft.com/en-us/library/ms186816.aspx + + + if (isset($rs->fields[5]) && $rs->fields[5]) { + if ($rs->fields[5]>0) $fld->max_length = $rs->fields[5]; + $fld->scale = $rs->fields[6]; + if ($fld->scale>0) $fld->max_length += 1; + } else + $fld->max_length = $rs->fields[2]; + + + if ($save == ADODB_FETCH_NUM) { + $retarr[] = $fld; + } else { + $retarr[strtoupper($fld->name)] = $fld; + } + $rs->MoveNext(); + } + + $rs->Close(); + return $retarr; + + } + + + function MetaIndexes($table,$primary=false, $owner=false) + { + $table = $this->qstr($table); + + $sql = "SELECT i.name AS ind_name, C.name AS col_name, USER_NAME(O.uid) AS Owner, c.colid, k.Keyno, + CASE WHEN I.indid BETWEEN 1 AND 254 AND (I.status & 2048 = 2048 OR I.Status = 16402 AND O.XType = 'V') THEN 1 ELSE 0 END AS IsPK, + CASE WHEN I.status & 2 = 2 THEN 1 ELSE 0 END AS IsUnique + FROM dbo.sysobjects o INNER JOIN dbo.sysindexes I ON o.id = i.id + INNER JOIN dbo.sysindexkeys K ON I.id = K.id AND I.Indid = K.Indid + INNER JOIN dbo.syscolumns c ON K.id = C.id AND K.colid = C.Colid + WHERE LEFT(i.name, 8) <> '_WA_Sys_' AND o.status >= 0 AND O.Name LIKE $table + ORDER BY O.name, I.Name, K.keyno"; + + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + + $rs = $this->Execute($sql); + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + if (!is_object($rs)) { + return FALSE; + } + + $indexes = array(); + while ($row = $rs->FetchRow()) { + if (!$primary && $row[5]) continue; + + $indexes[$row[0]]['unique'] = $row[6]; + $indexes[$row[0]]['columns'][] = $row[1]; + } + return $indexes; + } + + function _query($sql,$inputarr=false) + { + if (is_string($sql)) $sql = str_replace('||','+',$sql); + return ADODB_odbc::_query($sql,$inputarr); + } + + function SetTransactionMode( $transaction_mode ) + { + $this->_transmode = $transaction_mode; + if (empty($transaction_mode)) { + $this->Execute('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); + return; + } + if (!stristr($transaction_mode,'isolation')) $transaction_mode = 'ISOLATION LEVEL '.$transaction_mode; + $this->Execute("SET TRANSACTION ".$transaction_mode); + } + + // "Stein-Aksel Basma" + // tested with MSSQL 2000 + function MetaPrimaryKeys($table, $owner = false) + { + global $ADODB_FETCH_MODE; + + $schema = ''; + $this->_findschema($table,$schema); + //if (!$schema) $schema = $this->database; + if ($schema) $schema = "and k.table_catalog like '$schema%'"; + + $sql = "select distinct k.column_name,ordinal_position from information_schema.key_column_usage k, + information_schema.table_constraints tc + where tc.constraint_name = k.constraint_name and tc.constraint_type = + 'PRIMARY KEY' and k.table_name = '$table' $schema order by ordinal_position "; + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $a = $this->GetCol($sql); + $ADODB_FETCH_MODE = $savem; + + if ($a && sizeof($a)>0) return $a; + $false = false; + return $false; + } + + function SelectLimit($sql,$nrows=-1,$offset=-1, $inputarr=false,$secs2cache=0) + { + if ($nrows > 0 && $offset <= 0) { + $sql = preg_replace( + '/(^\s*select\s+(distinctrow|distinct)?)/i','\\1 '.$this->hasTop." $nrows ",$sql); + $rs = $this->Execute($sql,$inputarr); + } else + $rs = ADOConnection::SelectLimit($sql,$nrows,$offset,$inputarr,$secs2cache); + + return $rs; + } + + // Format date column in sql string given an input format that understands Y M D + function SQLDate($fmt, $col=false) + { + if (!$col) $col = $this->sysTimeStamp; + $s = ''; + + $len = strlen($fmt); + for ($i=0; $i < $len; $i++) { + if ($s) $s .= '+'; + $ch = $fmt[$i]; + switch($ch) { + case 'Y': + case 'y': + $s .= "datename(yyyy,$col)"; + break; + case 'M': + $s .= "convert(char(3),$col,0)"; + break; + case 'm': + $s .= "replace(str(month($col),2),' ','0')"; + break; + case 'Q': + case 'q': + $s .= "datename(quarter,$col)"; + break; + case 'D': + case 'd': + $s .= "replace(str(day($col),2),' ','0')"; + break; + case 'h': + $s .= "substring(convert(char(14),$col,0),13,2)"; + break; + + case 'H': + $s .= "replace(str(datepart(hh,$col),2),' ','0')"; + break; + + case 'i': + $s .= "replace(str(datepart(mi,$col),2),' ','0')"; + break; + case 's': + $s .= "replace(str(datepart(ss,$col),2),' ','0')"; + break; + case 'a': + case 'A': + $s .= "substring(convert(char(19),$col,0),18,2)"; + break; + + default: + if ($ch == '\\') { + $i++; + $ch = substr($fmt,$i,1); + } + $s .= $this->qstr($ch); + break; + } + } + return $s; + } + +} + +class ADORecordSet_odbc_mssql extends ADORecordSet_odbc { + + var $databaseType = 'odbc_mssql'; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-odbc_oracle.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-odbc_oracle.inc.php new file mode 100644 index 000000000..dfa851c0a --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-odbc_oracle.inc.php @@ -0,0 +1,111 @@ +Execute($this->metaTablesSQL); + if ($rs === false) return $false; + $arr = $rs->GetArray(); + $arr2 = array(); + for ($i=0; $i < sizeof($arr); $i++) { + $arr2[] = $arr[$i][0]; + } + $rs->Close(); + return $arr2; + } + + function MetaColumns($table, $normalize=true) + { + global $ADODB_FETCH_MODE; + + $rs = $this->Execute(sprintf($this->metaColumnsSQL,strtoupper($table))); + if ($rs === false) { + $false = false; + return $false; + } + $retarr = array(); + while (!$rs->EOF) { //print_r($rs->fields); + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; + $fld->type = $rs->fields[1]; + $fld->max_length = $rs->fields[2]; + + + if ($ADODB_FETCH_MODE == ADODB_FETCH_NUM) $retarr[] = $fld; + else $retarr[strtoupper($fld->name)] = $fld; + + $rs->MoveNext(); + } + $rs->Close(); + return $retarr; + } + + // returns true or false + function _connect($argDSN, $argUsername, $argPassword, $argDatabasename) + { + global $php_errormsg; + + $php_errormsg = ''; + $this->_connectionID = odbc_connect($argDSN,$argUsername,$argPassword,SQL_CUR_USE_ODBC ); + $this->_errorMsg = $php_errormsg; + + $this->Execute("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + //if ($this->_connectionID) odbc_autocommit($this->_connectionID,true); + return $this->_connectionID != false; + } + // returns true or false + function _pconnect($argDSN, $argUsername, $argPassword, $argDatabasename) + { + global $php_errormsg; + $php_errormsg = ''; + $this->_connectionID = odbc_pconnect($argDSN,$argUsername,$argPassword,SQL_CUR_USE_ODBC ); + $this->_errorMsg = $php_errormsg; + + $this->Execute("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + //if ($this->_connectionID) odbc_autocommit($this->_connectionID,true); + return $this->_connectionID != false; + } +} + +class ADORecordSet_odbc_oracle extends ADORecordSet_odbc { + + var $databaseType = 'odbc_oracle'; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-odbtp.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-odbtp.inc.php new file mode 100644 index 000000000..ec374a5db --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-odbtp.inc.php @@ -0,0 +1,840 @@ + + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +define("_ADODB_ODBTP_LAYER", 2 ); + +class ADODB_odbtp extends ADOConnection{ + var $databaseType = "odbtp"; + var $dataProvider = "odbtp"; + var $fmtDate = "'Y-m-d'"; + var $fmtTimeStamp = "'Y-m-d, h:i:sA'"; + var $replaceQuote = "''"; // string to use to replace quotes + var $odbc_driver = 0; + var $hasAffectedRows = true; + var $hasInsertID = false; + var $hasGenID = true; + var $hasMoveFirst = true; + + var $_genSeqSQL = "create table %s (seq_name char(30) not null unique , seq_value integer not null)"; + var $_dropSeqSQL = "delete from adodb_seq where seq_name = '%s'"; + var $_bindInputArray = false; + var $_useUnicodeSQL = false; + var $_canPrepareSP = false; + var $_dontPoolDBC = true; + + function __construct() + { + } + + function ServerInfo() + { + return array('description' => @odbtp_get_attr( ODB_ATTR_DBMSNAME, $this->_connectionID), + 'version' => @odbtp_get_attr( ODB_ATTR_DBMSVER, $this->_connectionID)); + } + + function ErrorMsg() + { + if ($this->_errorMsg !== false) return $this->_errorMsg; + if (empty($this->_connectionID)) return @odbtp_last_error(); + return @odbtp_last_error($this->_connectionID); + } + + function ErrorNo() + { + if ($this->_errorCode !== false) return $this->_errorCode; + if (empty($this->_connectionID)) return @odbtp_last_error_state(); + return @odbtp_last_error_state($this->_connectionID); + } +/* + function DBDate($d,$isfld=false) + { + if (empty($d) && $d !== 0) return 'null'; + if ($isfld) return "convert(date, $d, 120)"; + + if (is_string($d)) $d = ADORecordSet::UnixDate($d); + $d = adodb_date($this->fmtDate,$d); + return "convert(date, $d, 120)"; + } + + function DBTimeStamp($d,$isfld=false) + { + if (empty($d) && $d !== 0) return 'null'; + if ($isfld) return "convert(datetime, $d, 120)"; + + if (is_string($d)) $d = ADORecordSet::UnixDate($d); + $d = adodb_date($this->fmtDate,$d); + return "convert(datetime, $d, 120)"; + } +*/ + + function _insertid() + { + // SCOPE_IDENTITY() + // Returns the last IDENTITY value inserted into an IDENTITY column in + // the same scope. A scope is a module -- a stored procedure, trigger, + // function, or batch. Thus, two statements are in the same scope if + // they are in the same stored procedure, function, or batch. + return $this->GetOne($this->identitySQL); + } + + function _affectedrows() + { + if ($this->_queryID) { + return @odbtp_affected_rows ($this->_queryID); + } else + return 0; + } + + function CreateSequence($seqname='adodbseq',$start=1) + { + //verify existence + $num = $this->GetOne("select seq_value from adodb_seq"); + $seqtab='adodb_seq'; + if( $this->odbc_driver == ODB_DRIVER_FOXPRO ) { + $path = @odbtp_get_attr( ODB_ATTR_DATABASENAME, $this->_connectionID ); + //if using vfp dbc file + if( !strcasecmp(strrchr($path, '.'), '.dbc') ) + $path = substr($path,0,strrpos($path,'\/')); + $seqtab = $path . '/' . $seqtab; + } + if($num == false) { + if (empty($this->_genSeqSQL)) return false; + $ok = $this->Execute(sprintf($this->_genSeqSQL ,$seqtab)); + } + $num = $this->GetOne("select seq_value from adodb_seq where seq_name='$seqname'"); + if ($num) { + return false; + } + $start -= 1; + return $this->Execute("insert into adodb_seq values('$seqname',$start)"); + } + + function DropSequence($seqname = 'adodbseq') + { + if (empty($this->_dropSeqSQL)) return false; + return $this->Execute(sprintf($this->_dropSeqSQL,$seqname)); + } + + function GenID($seq='adodbseq',$start=1) + { + $seqtab='adodb_seq'; + if( $this->odbc_driver == ODB_DRIVER_FOXPRO) { + $path = @odbtp_get_attr( ODB_ATTR_DATABASENAME, $this->_connectionID ); + //if using vfp dbc file + if( !strcasecmp(strrchr($path, '.'), '.dbc') ) + $path = substr($path,0,strrpos($path,'\/')); + $seqtab = $path . '/' . $seqtab; + } + $MAXLOOPS = 100; + while (--$MAXLOOPS>=0) { + $num = $this->GetOne("select seq_value from adodb_seq where seq_name='$seq'"); + if ($num === false) { + //verify if abodb_seq table exist + $ok = $this->GetOne("select seq_value from adodb_seq "); + if(!$ok) { + //creating the sequence table adodb_seq + $this->Execute(sprintf($this->_genSeqSQL ,$seqtab)); + } + $start -= 1; + $num = '0'; + $ok = $this->Execute("insert into adodb_seq values('$seq',$start)"); + if (!$ok) return false; + } + $ok = $this->Execute("update adodb_seq set seq_value=seq_value+1 where seq_name='$seq'"); + if($ok) { + $num += 1; + $this->genID = $num; + return $num; + } + } + if ($fn = $this->raiseErrorFn) { + $fn($this->databaseType,'GENID',-32000,"Unable to generate unique id after $MAXLOOPS attempts",$seq,$num); + } + return false; + } + + //example for $UserOrDSN + //for visual fox : DRIVER={Microsoft Visual FoxPro Driver};SOURCETYPE=DBF;SOURCEDB=c:\YourDbfFileDir;EXCLUSIVE=NO; + //for visual fox dbc: DRIVER={Microsoft Visual FoxPro Driver};SOURCETYPE=DBC;SOURCEDB=c:\YourDbcFileDir\mydb.dbc;EXCLUSIVE=NO; + //for access : DRIVER={Microsoft Access Driver (*.mdb)};DBQ=c:\path_to_access_db\base_test.mdb;UID=root;PWD=; + //for mssql : DRIVER={SQL Server};SERVER=myserver;UID=myuid;PWD=mypwd;DATABASE=OdbtpTest; + //if uid & pwd can be separate + function _connect($HostOrInterface, $UserOrDSN='', $argPassword='', $argDatabase='') + { + if ($argPassword && stripos($UserOrDSN,'DRIVER=') !== false) { + $this->_connectionID = odbtp_connect($HostOrInterface,$UserOrDSN.';PWD='.$argPassword); + } else + $this->_connectionID = odbtp_connect($HostOrInterface,$UserOrDSN,$argPassword,$argDatabase); + if ($this->_connectionID === false) { + $this->_errorMsg = $this->ErrorMsg() ; + return false; + } + + odbtp_convert_datetime($this->_connectionID,true); + + if ($this->_dontPoolDBC) { + if (function_exists('odbtp_dont_pool_dbc')) + @odbtp_dont_pool_dbc($this->_connectionID); + } + else { + $this->_dontPoolDBC = true; + } + $this->odbc_driver = @odbtp_get_attr(ODB_ATTR_DRIVER, $this->_connectionID); + $dbms = strtolower(@odbtp_get_attr(ODB_ATTR_DBMSNAME, $this->_connectionID)); + $this->odbc_name = $dbms; + + // Account for inconsistent DBMS names + if( $this->odbc_driver == ODB_DRIVER_ORACLE ) + $dbms = 'oracle'; + else if( $this->odbc_driver == ODB_DRIVER_SYBASE ) + $dbms = 'sybase'; + + // Set DBMS specific attributes + switch( $dbms ) { + case 'microsoft sql server': + $this->databaseType = 'odbtp_mssql'; + $this->fmtDate = "'Y-m-d'"; + $this->fmtTimeStamp = "'Y-m-d h:i:sA'"; + $this->sysDate = 'convert(datetime,convert(char,GetDate(),102),102)'; + $this->sysTimeStamp = 'GetDate()'; + $this->ansiOuter = true; + $this->leftOuter = '*='; + $this->rightOuter = '=*'; + $this->hasTop = 'top'; + $this->hasInsertID = true; + $this->hasTransactions = true; + $this->_bindInputArray = true; + $this->_canSelectDb = true; + $this->substr = "substring"; + $this->length = 'len'; + $this->identitySQL = 'select SCOPE_IDENTITY()'; + $this->metaDatabasesSQL = "select name from master..sysdatabases where name <> 'master'"; + $this->_canPrepareSP = true; + break; + case 'access': + $this->databaseType = 'odbtp_access'; + $this->fmtDate = "#Y-m-d#"; + $this->fmtTimeStamp = "#Y-m-d h:i:sA#"; + $this->sysDate = "FORMAT(NOW,'yyyy-mm-dd')"; + $this->sysTimeStamp = 'NOW'; + $this->hasTop = 'top'; + $this->hasTransactions = false; + $this->_canPrepareSP = true; // For MS Access only. + break; + case 'visual foxpro': + $this->databaseType = 'odbtp_vfp'; + $this->fmtDate = "{^Y-m-d}"; + $this->fmtTimeStamp = "{^Y-m-d, h:i:sA}"; + $this->sysDate = 'date()'; + $this->sysTimeStamp = 'datetime()'; + $this->ansiOuter = true; + $this->hasTop = 'top'; + $this->hasTransactions = false; + $this->replaceQuote = "'+chr(39)+'"; + $this->true = '.T.'; + $this->false = '.F.'; + + break; + case 'oracle': + $this->databaseType = 'odbtp_oci8'; + $this->fmtDate = "'Y-m-d 00:00:00'"; + $this->fmtTimeStamp = "'Y-m-d h:i:sA'"; + $this->sysDate = 'TRUNC(SYSDATE)'; + $this->sysTimeStamp = 'SYSDATE'; + $this->hasTransactions = true; + $this->_bindInputArray = true; + $this->concat_operator = '||'; + break; + case 'sybase': + $this->databaseType = 'odbtp_sybase'; + $this->fmtDate = "'Y-m-d'"; + $this->fmtTimeStamp = "'Y-m-d H:i:s'"; + $this->sysDate = 'GetDate()'; + $this->sysTimeStamp = 'GetDate()'; + $this->leftOuter = '*='; + $this->rightOuter = '=*'; + $this->hasInsertID = true; + $this->hasTransactions = true; + $this->identitySQL = 'select SCOPE_IDENTITY()'; + break; + default: + $this->databaseType = 'odbtp'; + if( @odbtp_get_attr(ODB_ATTR_TXNCAPABLE, $this->_connectionID) ) + $this->hasTransactions = true; + else + $this->hasTransactions = false; + } + @odbtp_set_attr(ODB_ATTR_FULLCOLINFO, TRUE, $this->_connectionID ); + + if ($this->_useUnicodeSQL ) + @odbtp_set_attr(ODB_ATTR_UNICODESQL, TRUE, $this->_connectionID); + + return true; + } + + function _pconnect($HostOrInterface, $UserOrDSN='', $argPassword='', $argDatabase='') + { + $this->_dontPoolDBC = false; + return $this->_connect($HostOrInterface, $UserOrDSN, $argPassword, $argDatabase); + } + + function SelectDB($dbName) + { + if (!@odbtp_select_db($dbName, $this->_connectionID)) { + return false; + } + $this->database = $dbName; + $this->databaseName = $dbName; # obsolete, retained for compat with older adodb versions + return true; + } + + function MetaTables($ttype='',$showSchema=false,$mask=false) + { + global $ADODB_FETCH_MODE; + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== false) $savefm = $this->SetFetchMode(false); + + $arr = $this->GetArray("||SQLTables||||$ttype"); + + if (isset($savefm)) $this->SetFetchMode($savefm); + $ADODB_FETCH_MODE = $savem; + + $arr2 = array(); + for ($i=0; $i < sizeof($arr); $i++) { + if ($arr[$i][3] == 'SYSTEM TABLE' ) continue; + if ($arr[$i][2]) + $arr2[] = $showSchema && $arr[$i][1]? $arr[$i][1].'.'.$arr[$i][2] : $arr[$i][2]; + } + return $arr2; + } + + function MetaColumns($table,$upper=true) + { + global $ADODB_FETCH_MODE; + + $schema = false; + $this->_findschema($table,$schema); + if ($upper) $table = strtoupper($table); + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== false) $savefm = $this->SetFetchMode(false); + + $rs = $this->Execute( "||SQLColumns||$schema|$table" ); + + if (isset($savefm)) $this->SetFetchMode($savefm); + $ADODB_FETCH_MODE = $savem; + + if (!$rs || $rs->EOF) { + $false = false; + return $false; + } + $retarr = array(); + while (!$rs->EOF) { + //print_r($rs->fields); + if (strtoupper($rs->fields[2]) == $table) { + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[3]; + $fld->type = $rs->fields[5]; + $fld->max_length = $rs->fields[6]; + $fld->not_null = !empty($rs->fields[9]); + $fld->scale = $rs->fields[7]; + if (isset($rs->fields[12])) // vfp does not have field 12 + if (!is_null($rs->fields[12])) { + $fld->has_default = true; + $fld->default_value = $rs->fields[12]; + } + $retarr[strtoupper($fld->name)] = $fld; + } else if (!empty($retarr)) + break; + $rs->MoveNext(); + } + $rs->Close(); + + return $retarr; + } + + function MetaPrimaryKeys($table, $owner='') + { + global $ADODB_FETCH_MODE; + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $arr = $this->GetArray("||SQLPrimaryKeys||$owner|$table"); + $ADODB_FETCH_MODE = $savem; + + //print_r($arr); + $arr2 = array(); + for ($i=0; $i < sizeof($arr); $i++) { + if ($arr[$i][3]) $arr2[] = $arr[$i][3]; + } + return $arr2; + } + + function MetaForeignKeys($table, $owner='', $upper=false) + { + global $ADODB_FETCH_MODE; + + $savem = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $constraints = $this->GetArray("||SQLForeignKeys|||||$owner|$table"); + $ADODB_FETCH_MODE = $savem; + + $arr = false; + foreach($constraints as $constr) { + //print_r($constr); + $arr[$constr[11]][$constr[2]][] = $constr[7].'='.$constr[3]; + } + if (!$arr) { + $false = false; + return $false; + } + + $arr2 = array(); + + foreach($arr as $k => $v) { + foreach($v as $a => $b) { + if ($upper) $a = strtoupper($a); + $arr2[$a] = $b; + } + } + return $arr2; + } + + function BeginTrans() + { + if (!$this->hasTransactions) return false; + if ($this->transOff) return true; + $this->transCnt += 1; + $this->autoCommit = false; + if (defined('ODB_TXN_DEFAULT')) + $txn = ODB_TXN_DEFAULT; + else + $txn = ODB_TXN_READUNCOMMITTED; + $rs = @odbtp_set_attr(ODB_ATTR_TRANSACTIONS,$txn,$this->_connectionID); + if(!$rs) return false; + return true; + } + + function CommitTrans($ok=true) + { + if ($this->transOff) return true; + if (!$ok) return $this->RollbackTrans(); + if ($this->transCnt) $this->transCnt -= 1; + $this->autoCommit = true; + if( ($ret = @odbtp_commit($this->_connectionID)) ) + $ret = @odbtp_set_attr(ODB_ATTR_TRANSACTIONS, ODB_TXN_NONE, $this->_connectionID);//set transaction off + return $ret; + } + + function RollbackTrans() + { + if ($this->transOff) return true; + if ($this->transCnt) $this->transCnt -= 1; + $this->autoCommit = true; + if( ($ret = @odbtp_rollback($this->_connectionID)) ) + $ret = @odbtp_set_attr(ODB_ATTR_TRANSACTIONS, ODB_TXN_NONE, $this->_connectionID);//set transaction off + return $ret; + } + + function SelectLimit($sql,$nrows=-1,$offset=-1, $inputarr=false,$secs2cache=0) + { + // TOP requires ORDER BY for Visual FoxPro + if( $this->odbc_driver == ODB_DRIVER_FOXPRO ) { + if (!preg_match('/ORDER[ \t\r\n]+BY/is',$sql)) $sql .= ' ORDER BY 1'; + } + $ret = ADOConnection::SelectLimit($sql,$nrows,$offset,$inputarr,$secs2cache); + return $ret; + } + + function Prepare($sql) + { + if (! $this->_bindInputArray) return $sql; // no binding + + $this->_errorMsg = false; + $this->_errorCode = false; + + $stmt = @odbtp_prepare($sql,$this->_connectionID); + if (!$stmt) { + // print "Prepare Error for ($sql) ".$this->ErrorMsg()."
"; + return $sql; + } + return array($sql,$stmt,false); + } + + function PrepareSP($sql, $param = true) + { + if (!$this->_canPrepareSP) return $sql; // Can't prepare procedures + + $this->_errorMsg = false; + $this->_errorCode = false; + + $stmt = @odbtp_prepare_proc($sql,$this->_connectionID); + if (!$stmt) return false; + return array($sql,$stmt); + } + + /* + Usage: + $stmt = $db->PrepareSP('SP_RUNSOMETHING'); -- takes 2 params, @myid and @group + + # note that the parameter does not have @ in front! + $db->Parameter($stmt,$id,'myid'); + $db->Parameter($stmt,$group,'group',false,64); + $db->Parameter($stmt,$group,'photo',false,100000,ODB_BINARY); + $db->Execute($stmt); + + @param $stmt Statement returned by Prepare() or PrepareSP(). + @param $var PHP variable to bind to. Can set to null (for isNull support). + @param $name Name of stored procedure variable name to bind to. + @param [$isOutput] Indicates direction of parameter 0/false=IN 1=OUT 2= IN/OUT. This is ignored in odbtp. + @param [$maxLen] Holds an maximum length of the variable. + @param [$type] The data type of $var. Legal values depend on driver. + + See odbtp_attach_param documentation at http://odbtp.sourceforge.net. + */ + function Parameter(&$stmt, &$var, $name, $isOutput=false, $maxLen=0, $type=0) + { + if ( $this->odbc_driver == ODB_DRIVER_JET ) { + $name = '['.$name.']'; + if( !$type && $this->_useUnicodeSQL + && @odbtp_param_bindtype($stmt[1], $name) == ODB_CHAR ) + { + $type = ODB_WCHAR; + } + } + else { + $name = '@'.$name; + } + return @odbtp_attach_param($stmt[1], $name, $var, $type, $maxLen); + } + + /* + Insert a null into the blob field of the table first. + Then use UpdateBlob to store the blob. + + Usage: + + $conn->Execute('INSERT INTO blobtable (id, blobcol) VALUES (1, null)'); + $conn->UpdateBlob('blobtable','blobcol',$blob,'id=1'); + */ + + function UpdateBlob($table,$column,$val,$where,$blobtype='image') + { + $sql = "UPDATE $table SET $column = ? WHERE $where"; + if( !($stmt = @odbtp_prepare($sql, $this->_connectionID)) ) + return false; + if( !@odbtp_input( $stmt, 1, ODB_BINARY, 1000000, $blobtype ) ) + return false; + if( !@odbtp_set( $stmt, 1, $val ) ) + return false; + return @odbtp_execute( $stmt ) != false; + } + + function MetaIndexes($table,$primary=false, $owner=false) + { + switch ( $this->odbc_driver) { + case ODB_DRIVER_MSSQL: + return $this->MetaIndexes_mssql($table, $primary); + default: + return array(); + } + } + + function MetaIndexes_mssql($table,$primary=false, $owner = false) + { + $table = strtolower($this->qstr($table)); + + $sql = "SELECT i.name AS ind_name, C.name AS col_name, USER_NAME(O.uid) AS Owner, c.colid, k.Keyno, + CASE WHEN I.indid BETWEEN 1 AND 254 AND (I.status & 2048 = 2048 OR I.Status = 16402 AND O.XType = 'V') THEN 1 ELSE 0 END AS IsPK, + CASE WHEN I.status & 2 = 2 THEN 1 ELSE 0 END AS IsUnique + FROM dbo.sysobjects o INNER JOIN dbo.sysindexes I ON o.id = i.id + INNER JOIN dbo.sysindexkeys K ON I.id = K.id AND I.Indid = K.Indid + INNER JOIN dbo.syscolumns c ON K.id = C.id AND K.colid = C.Colid + WHERE LEFT(i.name, 8) <> '_WA_Sys_' AND o.status >= 0 AND lower(O.Name) = $table + ORDER BY O.name, I.Name, K.keyno"; + + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + + $rs = $this->Execute($sql); + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + if (!is_object($rs)) { + return FALSE; + } + + $indexes = array(); + while ($row = $rs->FetchRow()) { + if ($primary && !$row[5]) continue; + + $indexes[$row[0]]['unique'] = $row[6]; + $indexes[$row[0]]['columns'][] = $row[1]; + } + return $indexes; + } + + function IfNull( $field, $ifNull ) + { + switch( $this->odbc_driver ) { + case ODB_DRIVER_MSSQL: + return " ISNULL($field, $ifNull) "; + case ODB_DRIVER_JET: + return " IIF(IsNull($field), $ifNull, $field) "; + } + return " CASE WHEN $field is null THEN $ifNull ELSE $field END "; + } + + function _query($sql,$inputarr=false) + { + global $php_errormsg; + + $this->_errorMsg = false; + $this->_errorCode = false; + + if ($inputarr) { + if (is_array($sql)) { + $stmtid = $sql[1]; + } else { + $stmtid = @odbtp_prepare($sql,$this->_connectionID); + if ($stmtid == false) { + $this->_errorMsg = $php_errormsg; + return false; + } + } + $num_params = @odbtp_num_params( $stmtid ); + /* + for( $param = 1; $param <= $num_params; $param++ ) { + @odbtp_input( $stmtid, $param ); + @odbtp_set( $stmtid, $param, $inputarr[$param-1] ); + }*/ + + $param = 1; + foreach($inputarr as $v) { + @odbtp_input( $stmtid, $param ); + @odbtp_set( $stmtid, $param, $v ); + $param += 1; + if ($param > $num_params) break; + } + + if (!@odbtp_execute($stmtid) ) { + return false; + } + } else if (is_array($sql)) { + $stmtid = $sql[1]; + if (!@odbtp_execute($stmtid)) { + return false; + } + } else { + $stmtid = odbtp_query($sql,$this->_connectionID); + } + $this->_lastAffectedRows = 0; + if ($stmtid) { + $this->_lastAffectedRows = @odbtp_affected_rows($stmtid); + } + return $stmtid; + } + + function _close() + { + $ret = @odbtp_close($this->_connectionID); + $this->_connectionID = false; + return $ret; + } +} + +class ADORecordSet_odbtp extends ADORecordSet { + + var $databaseType = 'odbtp'; + var $canSeek = true; + + function __construct($queryID,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + $this->fetchMode = $mode; + parent::__construct($queryID); + } + + function _initrs() + { + $this->_numOfFields = @odbtp_num_fields($this->_queryID); + if (!($this->_numOfRows = @odbtp_num_rows($this->_queryID))) + $this->_numOfRows = -1; + + if (!$this->connection->_useUnicodeSQL) return; + + if ($this->connection->odbc_driver == ODB_DRIVER_JET) { + if (!@odbtp_get_attr(ODB_ATTR_MAPCHARTOWCHAR, + $this->connection->_connectionID)) + { + for ($f = 0; $f < $this->_numOfFields; $f++) { + if (@odbtp_field_bindtype($this->_queryID, $f) == ODB_CHAR) + @odbtp_bind_field($this->_queryID, $f, ODB_WCHAR); + } + } + } + } + + function FetchField($fieldOffset = 0) + { + $off=$fieldOffset; // offsets begin at 0 + $o= new ADOFieldObject(); + $o->name = @odbtp_field_name($this->_queryID,$off); + $o->type = @odbtp_field_type($this->_queryID,$off); + $o->max_length = @odbtp_field_length($this->_queryID,$off); + if (ADODB_ASSOC_CASE == 0) $o->name = strtolower($o->name); + else if (ADODB_ASSOC_CASE == 1) $o->name = strtoupper($o->name); + return $o; + } + + function _seek($row) + { + return @odbtp_data_seek($this->_queryID, $row); + } + + function fields($colname) + { + if ($this->fetchMode & ADODB_FETCH_ASSOC) return $this->fields[$colname]; + + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $name = @odbtp_field_name( $this->_queryID, $i ); + $this->bind[strtoupper($name)] = $i; + } + } + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + function _fetch_odbtp($type=0) + { + switch ($this->fetchMode) { + case ADODB_FETCH_NUM: + $this->fields = @odbtp_fetch_row($this->_queryID, $type); + break; + case ADODB_FETCH_ASSOC: + $this->fields = @odbtp_fetch_assoc($this->_queryID, $type); + break; + default: + $this->fields = @odbtp_fetch_array($this->_queryID, $type); + } + if ($this->databaseType = 'odbtp_vfp') { + if ($this->fields) + foreach($this->fields as $k => $v) { + if (strncmp($v,'1899-12-30',10) == 0) $this->fields[$k] = ''; + } + } + return is_array($this->fields); + } + + function _fetch() + { + return $this->_fetch_odbtp(); + } + + function MoveFirst() + { + if (!$this->_fetch_odbtp(ODB_FETCH_FIRST)) return false; + $this->EOF = false; + $this->_currentRow = 0; + return true; + } + + function MoveLast() + { + if (!$this->_fetch_odbtp(ODB_FETCH_LAST)) return false; + $this->EOF = false; + $this->_currentRow = $this->_numOfRows - 1; + return true; + } + + function NextRecordSet() + { + if (!@odbtp_next_result($this->_queryID)) return false; + $this->_inited = false; + $this->bind = false; + $this->_currentRow = -1; + $this->Init(); + return true; + } + + function _close() + { + return @odbtp_free_query($this->_queryID); + } +} + +class ADORecordSet_odbtp_mssql extends ADORecordSet_odbtp { + + var $databaseType = 'odbtp_mssql'; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +} + +class ADORecordSet_odbtp_access extends ADORecordSet_odbtp { + + var $databaseType = 'odbtp_access'; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +} + +class ADORecordSet_odbtp_vfp extends ADORecordSet_odbtp { + + var $databaseType = 'odbtp_vfp'; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +} + +class ADORecordSet_odbtp_oci8 extends ADORecordSet_odbtp { + + var $databaseType = 'odbtp_oci8'; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +} + +class ADORecordSet_odbtp_sybase extends ADORecordSet_odbtp { + + var $databaseType = 'odbtp_sybase'; + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-odbtp_unicode.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-odbtp_unicode.inc.php new file mode 100644 index 000000000..5ca03e717 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-odbtp_unicode.inc.php @@ -0,0 +1,35 @@ + + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +/* + Because the ODBTP server sends and reads UNICODE text data using UTF-8 + encoding, the following HTML meta tag must be included within the HTML + head section of every HTML form and script page: + + + + Also, all SQL query strings must be submitted as UTF-8 encoded text. +*/ + +if (!defined('_ADODB_ODBTP_LAYER')) { + include(ADODB_DIR."/drivers/adodb-odbtp.inc.php"); +} + +class ADODB_odbtp_unicode extends ADODB_odbtp { + var $databaseType = 'odbtp'; + var $_useUnicodeSQL = true; +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-oracle.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-oracle.inc.php new file mode 100644 index 000000000..ca737d668 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-oracle.inc.php @@ -0,0 +1,343 @@ +format($this->fmtDate); + else $ds = adodb_date($this->fmtDate,$d); + return 'TO_DATE('.$ds.",'YYYY-MM-DD')"; + } + + // format and return date string in database timestamp format + function DBTimeStamp($ts, $isfld = false) + { + + if (is_string($ts)) $ts = ADORecordSet::UnixTimeStamp($ts); + if (is_object($ts)) $ds = $ts->format($this->fmtDate); + else $ds = adodb_date($this->fmtTimeStamp,$ts); + return 'TO_DATE('.$ds.",'RRRR-MM-DD, HH:MI:SS AM')"; + } + + + function BindDate($d) + { + $d = ADOConnection::DBDate($d); + if (strncmp($d,"'",1)) return $d; + + return substr($d,1,strlen($d)-2); + } + + function BindTimeStamp($d) + { + $d = ADOConnection::DBTimeStamp($d); + if (strncmp($d,"'",1)) return $d; + + return substr($d,1,strlen($d)-2); + } + + + + function BeginTrans() + { + $this->autoCommit = false; + ora_commitoff($this->_connectionID); + return true; + } + + + function CommitTrans($ok=true) + { + if (!$ok) return $this->RollbackTrans(); + $ret = ora_commit($this->_connectionID); + ora_commiton($this->_connectionID); + return $ret; + } + + + function RollbackTrans() + { + $ret = ora_rollback($this->_connectionID); + ora_commiton($this->_connectionID); + return $ret; + } + + + /* there seems to be a bug in the oracle extension -- always returns ORA-00000 - no error */ + function ErrorMsg() + { + if ($this->_errorMsg !== false) return $this->_errorMsg; + + if (is_resource($this->_curs)) $this->_errorMsg = @ora_error($this->_curs); + if (empty($this->_errorMsg)) $this->_errorMsg = @ora_error($this->_connectionID); + return $this->_errorMsg; + } + + + function ErrorNo() + { + if ($this->_errorCode !== false) return $this->_errorCode; + + if (is_resource($this->_curs)) $this->_errorCode = @ora_errorcode($this->_curs); + if (empty($this->_errorCode)) $this->_errorCode = @ora_errorcode($this->_connectionID); + return $this->_errorCode; + } + + + + // returns true or false + function _connect($argHostname, $argUsername, $argPassword, $argDatabasename, $mode=0) + { + if (!function_exists('ora_plogon')) return null; + + // Reset error messages before connecting + $this->_errorMsg = false; + $this->_errorCode = false; + + // G. Giunta 2003/08/13 - This looks danegrously suspicious: why should we want to set + // the oracle home to the host name of remote DB? +// if ($argHostname) putenv("ORACLE_HOME=$argHostname"); + + if($argHostname) { // code copied from version submitted for oci8 by Jorma Tuomainen + if (empty($argDatabasename)) $argDatabasename = $argHostname; + else { + if(strpos($argHostname,":")) { + $argHostinfo=explode(":",$argHostname); + $argHostname=$argHostinfo[0]; + $argHostport=$argHostinfo[1]; + } else { + $argHostport="1521"; + } + + + if ($this->connectSID) { + $argDatabasename="(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=".$argHostname + .")(PORT=$argHostport))(CONNECT_DATA=(SID=$argDatabasename)))"; + } else + $argDatabasename="(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=".$argHostname + .")(PORT=$argHostport))(CONNECT_DATA=(SERVICE_NAME=$argDatabasename)))"; + } + + } + + if ($argDatabasename) $argUsername .= "@$argDatabasename"; + + //if ($argHostname) print "

Connect: 1st argument should be left blank for $this->databaseType

"; + if ($mode == 1) + $this->_connectionID = ora_plogon($argUsername,$argPassword); + else + $this->_connectionID = ora_logon($argUsername,$argPassword); + if ($this->_connectionID === false) return false; + if ($this->autoCommit) ora_commiton($this->_connectionID); + if ($this->_initdate) { + $rs = $this->_query("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD'"); + if ($rs) ora_close($rs); + } + + return true; + } + + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabasename, 1); + } + + + // returns query ID if successful, otherwise false + function _query($sql,$inputarr=false) + { + // Reset error messages before executing + $this->_errorMsg = false; + $this->_errorCode = false; + + $curs = ora_open($this->_connectionID); + + if ($curs === false) return false; + $this->_curs = $curs; + if (!ora_parse($curs,$sql)) return false; + if (ora_exec($curs)) return $curs; + // before we close the cursor, we have to store the error message + // that we can obtain ONLY from the cursor (and not from the connection) + $this->_errorCode = @ora_errorcode($curs); + $this->_errorMsg = @ora_error($curs); + // + @ora_close($curs); + return false; + } + + + // returns true or false + function _close() + { + return @ora_logoff($this->_connectionID); + } + + + +} + + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordset_oracle extends ADORecordSet { + + var $databaseType = "oracle"; + var $bind = false; + + function __construct($queryID,$mode=false) + { + + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + $this->fetchMode = $mode; + + $this->_queryID = $queryID; + + $this->_inited = true; + $this->fields = array(); + if ($queryID) { + $this->_currentRow = 0; + $this->EOF = !$this->_fetch(); + @$this->_initrs(); + } else { + $this->_numOfRows = 0; + $this->_numOfFields = 0; + $this->EOF = true; + } + + return $this->_queryID; + } + + + + /* Returns: an object containing field information. + Get column information in the Recordset object. fetchField() can be used in order to obtain information about + fields in a certain query result. If the field offset isn't specified, the next field that wasn't yet retrieved by + fetchField() is retrieved. */ + + function FetchField($fieldOffset = -1) + { + $fld = new ADOFieldObject; + $fld->name = ora_columnname($this->_queryID, $fieldOffset); + $fld->type = ora_columntype($this->_queryID, $fieldOffset); + $fld->max_length = ora_columnsize($this->_queryID, $fieldOffset); + return $fld; + } + + /* Use associative array to get fields array */ + function Fields($colname) + { + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + function _initrs() + { + $this->_numOfRows = -1; + $this->_numOfFields = @ora_numcols($this->_queryID); + } + + + function _seek($row) + { + return false; + } + + function _fetch($ignore_fields=false) { +// should remove call by reference, but ora_fetch_into requires it in 4.0.3pl1 + if ($this->fetchMode & ADODB_FETCH_ASSOC) + return @ora_fetch_into($this->_queryID,$this->fields,ORA_FETCHINTO_NULLS|ORA_FETCHINTO_ASSOC); + else + return @ora_fetch_into($this->_queryID,$this->fields,ORA_FETCHINTO_NULLS); + } + + /* close() only needs to be called if you are worried about using too much memory while your script + is running. All associated result memory for the specified result identifier will automatically be freed. */ + + function _close() +{ + return @ora_close($this->_queryID); + } + + function MetaType($t, $len = -1, $fieldobj = false) + { + if (is_object($t)) { + $fieldobj = $t; + $t = $fieldobj->type; + $len = $fieldobj->max_length; + } + + switch (strtoupper($t)) { + case 'VARCHAR': + case 'VARCHAR2': + case 'CHAR': + case 'VARBINARY': + case 'BINARY': + if ($len <= $this->blobSize) return 'C'; + case 'LONG': + case 'LONG VARCHAR': + case 'CLOB': + return 'X'; + case 'LONG RAW': + case 'LONG VARBINARY': + case 'BLOB': + return 'B'; + + case 'DATE': return 'D'; + + //case 'T': return 'T'; + + case 'BIT': return 'L'; + case 'INT': + case 'SMALLINT': + case 'INTEGER': return 'I'; + default: return 'N'; + } + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-pdo.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-pdo.inc.php new file mode 100644 index 000000000..b7b5577fa --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-pdo.inc.php @@ -0,0 +1,803 @@ +_driver; + $this->fmtDate = $d->fmtDate; + $this->fmtTimeStamp = $d->fmtTimeStamp; + $this->replaceQuote = $d->replaceQuote; + $this->sysDate = $d->sysDate; + $this->sysTimeStamp = $d->sysTimeStamp; + $this->random = $d->random; + $this->concat_operator = $d->concat_operator; + $this->nameQuote = $d->nameQuote; + + $this->hasGenID = $d->hasGenID; + $this->_genIDSQL = $d->_genIDSQL; + $this->_genSeqSQL = $d->_genSeqSQL; + $this->_dropSeqSQL = $d->_dropSeqSQL; + + $d->_init($this); + } + + function Time() + { + if (!empty($this->_driver->_hasdual)) { + $sql = "select $this->sysTimeStamp from dual"; + } + else { + $sql = "select $this->sysTimeStamp"; + } + + $rs = $this->_Execute($sql); + if ($rs && !$rs->EOF) { + return $this->UnixTimeStamp(reset($rs->fields)); + } + + return false; + } + + // returns true or false + function _connect($argDSN, $argUsername, $argPassword, $argDatabasename, $persist=false) + { + $at = strpos($argDSN,':'); + $this->dsnType = substr($argDSN,0,$at); + + if ($argDatabasename) { + switch($this->dsnType){ + case 'sqlsrv': + $argDSN .= ';database='.$argDatabasename; + break; + case 'mssql': + case 'mysql': + case 'oci': + case 'pgsql': + case 'sqlite': + default: + $argDSN .= ';dbname='.$argDatabasename; + } + } + try { + $this->_connectionID = new PDO($argDSN, $argUsername, $argPassword); + } catch (Exception $e) { + $this->_connectionID = false; + $this->_errorno = -1; + //var_dump($e); + $this->_errormsg = 'Connection attempt failed: '.$e->getMessage(); + return false; + } + + if ($this->_connectionID) { + switch(ADODB_ASSOC_CASE){ + case ADODB_ASSOC_CASE_LOWER: + $m = PDO::CASE_LOWER; + break; + case ADODB_ASSOC_CASE_UPPER: + $m = PDO::CASE_UPPER; + break; + default: + case ADODB_ASSOC_CASE_NATIVE: + $m = PDO::CASE_NATURAL; + break; + } + + //$this->_connectionID->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT ); + $this->_connectionID->setAttribute(PDO::ATTR_CASE,$m); + + $class = 'ADODB_pdo_'.$this->dsnType; + //$this->_connectionID->setAttribute(PDO::ATTR_AUTOCOMMIT,true); + switch($this->dsnType) { + case 'mssql': + case 'mysql': + case 'oci': + case 'pgsql': + case 'sqlite': + case 'sqlsrv': + include_once(ADODB_DIR.'/drivers/adodb-pdo_'.$this->dsnType.'.inc.php'); + break; + } + if (class_exists($class)) { + $this->_driver = new $class(); + } + else { + $this->_driver = new ADODB_pdo_base(); + } + + $this->_driver->_connectionID = $this->_connectionID; + $this->_UpdatePDO(); + $this->_driver->database = $this->database; + return true; + } + $this->_driver = new ADODB_pdo_base(); + return false; + } + + function Concat() + { + $args = func_get_args(); + if(method_exists($this->_driver, 'Concat')) { + return call_user_func_array(array($this->_driver, 'Concat'), $args); + } + + if (PHP_VERSION >= 5.3) { + return call_user_func_array('parent::Concat', $args); + } + return call_user_func_array(array($this,'parent::Concat'), $args); + } + + // returns true or false + function _pconnect($argDSN, $argUsername, $argPassword, $argDatabasename) + { + return $this->_connect($argDSN, $argUsername, $argPassword, $argDatabasename, true); + } + + /*------------------------------------------------------------------------------*/ + + + function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false,$secs2cache=0) + { + $save = $this->_driver->fetchMode; + $this->_driver->fetchMode = $this->fetchMode; + $this->_driver->debug = $this->debug; + $ret = $this->_driver->SelectLimit($sql,$nrows,$offset,$inputarr,$secs2cache); + $this->_driver->fetchMode = $save; + return $ret; + } + + + function ServerInfo() + { + return $this->_driver->ServerInfo(); + } + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + return $this->_driver->MetaTables($ttype,$showSchema,$mask); + } + + function MetaColumns($table,$normalize=true) + { + return $this->_driver->MetaColumns($table,$normalize); + } + + function InParameter(&$stmt,&$var,$name,$maxLen=4000,$type=false) + { + $obj = $stmt[1]; + if ($type) { + $obj->bindParam($name, $var, $type, $maxLen); + } + else { + $obj->bindParam($name, $var); + } + } + + function OffsetDate($dayFraction,$date=false) + { + return $this->_driver->OffsetDate($dayFraction,$date); + } + + function SelectDB($dbName) + { + return $this->_driver->SelectDB($dbName); + } + + function SQLDate($fmt, $col=false) + { + return $this->_driver->SQLDate($fmt, $col); + } + + function ErrorMsg() + { + if ($this->_errormsg !== false) { + return $this->_errormsg; + } + if (!empty($this->_stmt)) { + $arr = $this->_stmt->errorInfo(); + } + else if (!empty($this->_connectionID)) { + $arr = $this->_connectionID->errorInfo(); + } + else { + return 'No Connection Established'; + } + + if ($arr) { + if (sizeof($arr)<2) { + return ''; + } + if ((integer)$arr[0]) { + return $arr[2]; + } + else { + return ''; + } + } + else { + return '-1'; + } + } + + + function ErrorNo() + { + if ($this->_errorno !== false) { + return $this->_errorno; + } + if (!empty($this->_stmt)) { + $err = $this->_stmt->errorCode(); + } + else if (!empty($this->_connectionID)) { + $arr = $this->_connectionID->errorInfo(); + if (isset($arr[0])) { + $err = $arr[0]; + } + else { + $err = -1; + } + } else { + return 0; + } + + if ($err == '00000') { + return 0; // allows empty check + } + return $err; + } + + function SetTransactionMode($transaction_mode) + { + if(method_exists($this->_driver, 'SetTransactionMode')) { + return $this->_driver->SetTransactionMode($transaction_mode); + } + + return parent::SetTransactionMode($seqname); + } + + function BeginTrans() + { + if(method_exists($this->_driver, 'BeginTrans')) { + return $this->_driver->BeginTrans(); + } + + if (!$this->hasTransactions) { + return false; + } + if ($this->transOff) { + return true; + } + $this->transCnt += 1; + $this->_autocommit = false; + $this->_connectionID->setAttribute(PDO::ATTR_AUTOCOMMIT,false); + + return $this->_connectionID->beginTransaction(); + } + + function CommitTrans($ok=true) + { + if(method_exists($this->_driver, 'CommitTrans')) { + return $this->_driver->CommitTrans($ok); + } + + if (!$this->hasTransactions) { + return false; + } + if ($this->transOff) { + return true; + } + if (!$ok) { + return $this->RollbackTrans(); + } + if ($this->transCnt) { + $this->transCnt -= 1; + } + $this->_autocommit = true; + + $ret = $this->_connectionID->commit(); + $this->_connectionID->setAttribute(PDO::ATTR_AUTOCOMMIT,true); + return $ret; + } + + function RollbackTrans() + { + if(method_exists($this->_driver, 'RollbackTrans')) { + return $this->_driver->RollbackTrans(); + } + + if (!$this->hasTransactions) { + return false; + } + if ($this->transOff) { + return true; + } + if ($this->transCnt) { + $this->transCnt -= 1; + } + $this->_autocommit = true; + + $ret = $this->_connectionID->rollback(); + $this->_connectionID->setAttribute(PDO::ATTR_AUTOCOMMIT,true); + return $ret; + } + + function Prepare($sql) + { + $this->_stmt = $this->_connectionID->prepare($sql); + if ($this->_stmt) { + return array($sql,$this->_stmt); + } + + return false; + } + + function PrepareStmt($sql) + { + $stmt = $this->_connectionID->prepare($sql); + if (!$stmt) { + return false; + } + $obj = new ADOPDOStatement($stmt,$this); + return $obj; + } + + function CreateSequence($seqname='adodbseq',$startID=1) + { + if(method_exists($this->_driver, 'CreateSequence')) { + return $this->_driver->CreateSequence($seqname, $startID); + } + + return parent::CreateSequence($seqname, $startID); + } + + function DropSequence($seqname='adodbseq') + { + if(method_exists($this->_driver, 'DropSequence')) { + return $this->_driver->DropSequence($seqname); + } + + return parent::DropSequence($seqname); + } + + function GenID($seqname='adodbseq',$startID=1) + { + if(method_exists($this->_driver, 'GenID')) { + return $this->_driver->GenID($seqname, $startID); + } + + return parent::GenID($seqname, $startID); + } + + + /* returns queryID or false */ + function _query($sql,$inputarr=false) + { + if (is_array($sql)) { + $stmt = $sql[1]; + } else { + $stmt = $this->_connectionID->prepare($sql); + } + #adodb_backtrace(); + #var_dump($this->_bindInputArray); + if ($stmt) { + $this->_driver->debug = $this->debug; + if ($inputarr) { + $ok = $stmt->execute($inputarr); + } + else { + $ok = $stmt->execute(); + } + } + + + $this->_errormsg = false; + $this->_errorno = false; + + if ($ok) { + $this->_stmt = $stmt; + return $stmt; + } + + if ($stmt) { + + $arr = $stmt->errorinfo(); + if ((integer)$arr[1]) { + $this->_errormsg = $arr[2]; + $this->_errorno = $arr[1]; + } + + } else { + $this->_errormsg = false; + $this->_errorno = false; + } + return false; + } + + // returns true or false + function _close() + { + $this->_stmt = false; + return true; + } + + function _affectedrows() + { + return ($this->_stmt) ? $this->_stmt->rowCount() : 0; + } + + function _insertid() + { + return ($this->_connectionID) ? $this->_connectionID->lastInsertId() : 0; + } + + /** + * Quotes a string to be sent to the database. + * If we have an active connection, delegates quoting to the underlying + * PDO object. Otherwise, replace "'" by the value of $replaceQuote (same + * behavior as mysqli driver) + * @param string $s The string to quote + * @param boolean $magic_quotes If false, use PDO::quote(). + * @return string Quoted string + */ + function qstr($s, $magic_quotes = false) + { + if (!$magic_quotes) { + if ($this->_connectionID) { + return $this->_connectionID->quote($s); + } + return "'" . str_replace("'", $this->replaceQuote, $s) . "'"; + } + + // undo magic quotes for " + $s = str_replace('\\"', '"', $s); + return "'$s'"; + } + +} + +class ADODB_pdo_base extends ADODB_pdo { + + var $sysDate = "'?'"; + var $sysTimeStamp = "'?'"; + + + function _init($parentDriver) + { + $parentDriver->_bindInputArray = true; + #$parentDriver->_connectionID->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY,true); + } + + function ServerInfo() + { + return ADOConnection::ServerInfo(); + } + + function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false,$secs2cache=0) + { + $ret = ADOConnection::SelectLimit($sql,$nrows,$offset,$inputarr,$secs2cache); + return $ret; + } + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + return false; + } + + function MetaColumns($table,$normalize=true) + { + return false; + } +} + +class ADOPDOStatement { + + var $databaseType = "pdo"; + var $dataProvider = "pdo"; + var $_stmt; + var $_connectionID; + + function __construct($stmt,$connection) + { + $this->_stmt = $stmt; + $this->_connectionID = $connection; + } + + function Execute($inputArr=false) + { + $savestmt = $this->_connectionID->_stmt; + $rs = $this->_connectionID->Execute(array(false,$this->_stmt),$inputArr); + $this->_connectionID->_stmt = $savestmt; + return $rs; + } + + function InParameter(&$var,$name,$maxLen=4000,$type=false) + { + + if ($type) { + $this->_stmt->bindParam($name,$var,$type,$maxLen); + } + else { + $this->_stmt->bindParam($name, $var); + } + } + + function Affected_Rows() + { + return ($this->_stmt) ? $this->_stmt->rowCount() : 0; + } + + function ErrorMsg() + { + if ($this->_stmt) { + $arr = $this->_stmt->errorInfo(); + } + else { + $arr = $this->_connectionID->errorInfo(); + } + + if (is_array($arr)) { + if ((integer) $arr[0] && isset($arr[2])) { + return $arr[2]; + } + else { + return ''; + } + } else { + return '-1'; + } + } + + function NumCols() + { + return ($this->_stmt) ? $this->_stmt->columnCount() : 0; + } + + function ErrorNo() + { + if ($this->_stmt) { + return $this->_stmt->errorCode(); + } + else { + return $this->_connectionID->errorInfo(); + } + } +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordSet_pdo extends ADORecordSet { + + var $bind = false; + var $databaseType = "pdo"; + var $dataProvider = "pdo"; + + function __construct($id,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + $this->adodbFetchMode = $mode; + switch($mode) { + case ADODB_FETCH_NUM: $mode = PDO::FETCH_NUM; break; + case ADODB_FETCH_ASSOC: $mode = PDO::FETCH_ASSOC; break; + + case ADODB_FETCH_BOTH: + default: $mode = PDO::FETCH_BOTH; break; + } + $this->fetchMode = $mode; + + $this->_queryID = $id; + parent::__construct($id); + } + + + function Init() + { + if ($this->_inited) { + return; + } + $this->_inited = true; + if ($this->_queryID) { + @$this->_initrs(); + } + else { + $this->_numOfRows = 0; + $this->_numOfFields = 0; + } + if ($this->_numOfRows != 0 && $this->_currentRow == -1) { + $this->_currentRow = 0; + if ($this->EOF = ($this->_fetch() === false)) { + $this->_numOfRows = 0; // _numOfRows could be -1 + } + } else { + $this->EOF = true; + } + } + + function _initrs() + { + global $ADODB_COUNTRECS; + + $this->_numOfRows = ($ADODB_COUNTRECS) ? @$this->_queryID->rowCount() : -1; + if (!$this->_numOfRows) { + $this->_numOfRows = -1; + } + $this->_numOfFields = $this->_queryID->columnCount(); + } + + // returns the field object + function FetchField($fieldOffset = -1) + { + $off=$fieldOffset+1; // offsets begin at 1 + + $o= new ADOFieldObject(); + $arr = @$this->_queryID->getColumnMeta($fieldOffset); + if (!$arr) { + $o->name = 'bad getColumnMeta()'; + $o->max_length = -1; + $o->type = 'VARCHAR'; + $o->precision = 0; + # $false = false; + return $o; + } + //adodb_pr($arr); + $o->name = $arr['name']; + if (isset($arr['sqlsrv:decl_type']) && $arr['sqlsrv:decl_type'] <> "null") + { + /* + * If the database is SQL server, use the native built-ins + */ + $o->type = $arr['sqlsrv:decl_type']; + } + elseif (isset($arr['native_type']) && $arr['native_type'] <> "null") + { + $o->type = $arr['native_type']; + } + else + { + $o->type = adodb_pdo_type($arr['pdo_type']); + } + + $o->max_length = $arr['len']; + $o->precision = $arr['precision']; + + switch(ADODB_ASSOC_CASE) { + case ADODB_ASSOC_CASE_LOWER: + $o->name = strtolower($o->name); + break; + case ADODB_ASSOC_CASE_UPPER: + $o->name = strtoupper($o->name); + break; + } + return $o; + } + + function _seek($row) + { + return false; + } + + function _fetch() + { + if (!$this->_queryID) { + return false; + } + + $this->fields = $this->_queryID->fetch($this->fetchMode); + return !empty($this->fields); + } + + function _close() + { + $this->_queryID = false; + } + + function Fields($colname) + { + if ($this->adodbFetchMode != ADODB_FETCH_NUM) { + return @$this->fields[$colname]; + } + + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + return $this->fields[$this->bind[strtoupper($colname)]]; + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-pdo_mssql.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_mssql.inc.php new file mode 100644 index 000000000..219cf1312 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_mssql.inc.php @@ -0,0 +1,62 @@ +hasTransactions = false; ## <<< BUG IN PDO mssql driver + $parentDriver->_bindInputArray = false; + $parentDriver->hasInsertID = true; + } + + function ServerInfo() + { + return ADOConnection::ServerInfo(); + } + + function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false,$secs2cache=0) + { + $ret = ADOConnection::SelectLimit($sql,$nrows,$offset,$inputarr,$secs2cache); + return $ret; + } + + function SetTransactionMode( $transaction_mode ) + { + $this->_transmode = $transaction_mode; + if (empty($transaction_mode)) { + $this->Execute('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); + return; + } + if (!stristr($transaction_mode,'isolation')) $transaction_mode = 'ISOLATION LEVEL '.$transaction_mode; + $this->Execute("SET TRANSACTION ".$transaction_mode); + } + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + return false; + } + + function MetaColumns($table,$normalize=true) + { + return false; + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-pdo_mysql.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_mysql.inc.php new file mode 100644 index 000000000..36d97b6ec --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_mysql.inc.php @@ -0,0 +1,302 @@ +hasTransactions = false; + #$parentDriver->_bindInputArray = false; + $parentDriver->hasInsertID = true; + $parentDriver->_connectionID->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); + } + + // dayFraction is a day in floating point + function OffsetDate($dayFraction, $date=false) + { + if (!$date) { + $date = $this->sysDate; + } + + $fraction = $dayFraction * 24 * 3600; + return $date . ' + INTERVAL ' . $fraction . ' SECOND'; +// return "from_unixtime(unix_timestamp($date)+$fraction)"; + } + + function Concat() + { + $s = ''; + $arr = func_get_args(); + + // suggestion by andrew005#mnogo.ru + $s = implode(',', $arr); + if (strlen($s) > 0) { + return "CONCAT($s)"; + } + return ''; + } + + function ServerInfo() + { + $arr['description'] = ADOConnection::GetOne('select version()'); + $arr['version'] = ADOConnection::_findvers($arr['description']); + return $arr; + } + + function MetaTables($ttype=false, $showSchema=false, $mask=false) + { + $save = $this->metaTablesSQL; + if ($showSchema && is_string($showSchema)) { + $this->metaTablesSQL .= $this->qstr($showSchema); + } else { + $this->metaTablesSQL .= 'schema()'; + } + + if ($mask) { + $mask = $this->qstr($mask); + $this->metaTablesSQL .= " like $mask"; + } + $ret = ADOConnection::MetaTables($ttype, $showSchema); + + $this->metaTablesSQL = $save; + return $ret; + } + + function SetTransactionMode($transaction_mode) + { + $this->_transmode = $transaction_mode; + if (empty($transaction_mode)) { + $this->Execute('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ'); + return; + } + if (!stristr($transaction_mode, 'isolation')) { + $transaction_mode = 'ISOLATION LEVEL ' . $transaction_mode; + } + $this->Execute('SET SESSION TRANSACTION ' . $transaction_mode); + } + + function MetaColumns($table, $normalize=true) + { + $this->_findschema($table, $schema); + if ($schema) { + $dbName = $this->database; + $this->SelectDB($schema); + } + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + if ($this->fetchMode !== false) { + $savem = $this->SetFetchMode(false); + } + $rs = $this->Execute(sprintf($this->metaColumnsSQL, $table)); + + if ($schema) { + $this->SelectDB($dbName); + } + + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + if (!is_object($rs)) { + $false = false; + return $false; + } + + $retarr = array(); + while (!$rs->EOF){ + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; + $type = $rs->fields[1]; + + // split type into type(length): + $fld->scale = null; + if (preg_match('/^(.+)\((\d+),(\d+)/', $type, $query_array)) { + $fld->type = $query_array[1]; + $fld->max_length = is_numeric($query_array[2]) ? $query_array[2] : -1; + $fld->scale = is_numeric($query_array[3]) ? $query_array[3] : -1; + } elseif (preg_match('/^(.+)\((\d+)/', $type, $query_array)) { + $fld->type = $query_array[1]; + $fld->max_length = is_numeric($query_array[2]) ? $query_array[2] : -1; + } elseif (preg_match('/^(enum)\((.*)\)$/i', $type, $query_array)) { + $fld->type = $query_array[1]; + $arr = explode(',', $query_array[2]); + $fld->enums = $arr; + $zlen = max(array_map('strlen', $arr)) - 2; // PHP >= 4.0.6 + $fld->max_length = ($zlen > 0) ? $zlen : 1; + } else { + $fld->type = $type; + $fld->max_length = -1; + } + $fld->not_null = ($rs->fields[2] != 'YES'); + $fld->primary_key = ($rs->fields[3] == 'PRI'); + $fld->auto_increment = (strpos($rs->fields[5], 'auto_increment') !== false); + $fld->binary = (strpos($type, 'blob') !== false); + $fld->unsigned = (strpos($type, 'unsigned') !== false); + + if (!$fld->binary) { + $d = $rs->fields[4]; + if ($d != '' && $d != 'NULL') { + $fld->has_default = true; + $fld->default_value = $d; + } else { + $fld->has_default = false; + } + } + + if ($save == ADODB_FETCH_NUM) { + $retarr[] = $fld; + } else { + $retarr[strtoupper($fld->name)] = $fld; + } + $rs->MoveNext(); + } + + $rs->Close(); + return $retarr; + } + + // returns true or false + function SelectDB($dbName) + { + $this->database = $dbName; + $this->databaseName = $dbName; # obsolete, retained for compat with older adodb versions + $try = $this->Execute('use ' . $dbName); + return ($try !== false); + } + + // parameters use PostgreSQL convention, not MySQL + function SelectLimit($sql, $nrows=-1, $offset=-1, $inputarr=false, $secs=0) + { + $offsetStr =($offset>=0) ? "$offset," : ''; + // jason judge, see http://phplens.com/lens/lensforum/msgs.php?id=9220 + if ($nrows < 0) { + $nrows = '18446744073709551615'; + } + + if ($secs) { + $rs = $this->CacheExecute($secs, $sql . " LIMIT $offsetStr$nrows", $inputarr); + } else { + $rs = $this->Execute($sql . " LIMIT $offsetStr$nrows", $inputarr); + } + return $rs; + } + + function SQLDate($fmt, $col=false) + { + if (!$col) { + $col = $this->sysTimeStamp; + } + $s = 'DATE_FORMAT(' . $col . ",'"; + $concat = false; + $len = strlen($fmt); + for ($i=0; $i < $len; $i++) { + $ch = $fmt[$i]; + switch($ch) { + + default: + if ($ch == '\\') { + $i++; + $ch = substr($fmt, $i, 1); + } + // FALL THROUGH + case '-': + case '/': + $s .= $ch; + break; + + case 'Y': + case 'y': + $s .= '%Y'; + break; + + case 'M': + $s .= '%b'; + break; + + case 'm': + $s .= '%m'; + break; + + case 'D': + case 'd': + $s .= '%d'; + break; + + case 'Q': + case 'q': + $s .= "'),Quarter($col)"; + + if ($len > $i+1) { + $s .= ",DATE_FORMAT($col,'"; + } else { + $s .= ",('"; + } + $concat = true; + break; + + case 'H': + $s .= '%H'; + break; + + case 'h': + $s .= '%I'; + break; + + case 'i': + $s .= '%i'; + break; + + case 's': + $s .= '%s'; + break; + + case 'a': + case 'A': + $s .= '%p'; + break; + + case 'w': + $s .= '%w'; + break; + + case 'W': + $s .= '%U'; + break; + + case 'l': + $s .= '%W'; + break; + } + } + $s .= "')"; + if ($concat) { + $s = "CONCAT($s)"; + } + return $s; + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-pdo_oci.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_oci.inc.php new file mode 100644 index 000000000..0bca35e3c --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_oci.inc.php @@ -0,0 +1,93 @@ +_bindInputArray = true; + $parentDriver->_nestedSQL = true; + if ($this->_initdate) { + $parentDriver->Execute("ALTER SESSION SET NLS_DATE_FORMAT='".$this->NLS_DATE_FORMAT."'"); + } + } + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + if ($mask) { + $save = $this->metaTablesSQL; + $mask = $this->qstr(strtoupper($mask)); + $this->metaTablesSQL .= " AND table_name like $mask"; + } + $ret = ADOConnection::MetaTables($ttype,$showSchema); + + if ($mask) { + $this->metaTablesSQL = $save; + } + return $ret; + } + + function MetaColumns($table,$normalize=true) + { + global $ADODB_FETCH_MODE; + + $false = false; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== false) $savem = $this->SetFetchMode(false); + + $rs = $this->Execute(sprintf($this->metaColumnsSQL,strtoupper($table))); + + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + if (!$rs) { + return $false; + } + $retarr = array(); + while (!$rs->EOF) { //print_r($rs->fields); + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; + $fld->type = $rs->fields[1]; + $fld->max_length = $rs->fields[2]; + $fld->scale = $rs->fields[3]; + if ($rs->fields[1] == 'NUMBER' && $rs->fields[3] == 0) { + $fld->type ='INT'; + $fld->max_length = $rs->fields[4]; + } + $fld->not_null = (strncmp($rs->fields[5], 'NOT',3) === 0); + $fld->binary = (strpos($fld->type,'BLOB') !== false); + $fld->default_value = $rs->fields[6]; + + if ($ADODB_FETCH_MODE == ADODB_FETCH_NUM) $retarr[] = $fld; + else $retarr[strtoupper($fld->name)] = $fld; + $rs->MoveNext(); + } + $rs->Close(); + if (empty($retarr)) + return $false; + else + return $retarr; + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-pdo_pgsql.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_pgsql.inc.php new file mode 100644 index 000000000..9afe4b0e4 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_pgsql.inc.php @@ -0,0 +1,230 @@ + 0 AND a.atttypid = t.oid AND a.attrelid = c.oid ORDER BY a.attnum"; + + // used when schema defined + var $metaColumnsSQL1 = "SELECT a.attname, t.typname, a.attlen, a.atttypmod, a.attnotnull, a.atthasdef, a.attnum +FROM pg_class c, pg_attribute a, pg_type t, pg_namespace n +WHERE relkind in ('r','v') AND (c.relname='%s' or c.relname = lower('%s')) + and c.relnamespace=n.oid and n.nspname='%s' + and a.attname not like '....%%' AND a.attnum > 0 + AND a.atttypid = t.oid AND a.attrelid = c.oid ORDER BY a.attnum"; + + // get primary key etc -- from Freek Dijkstra + var $metaKeySQL = "SELECT ic.relname AS index_name, a.attname AS column_name,i.indisunique AS unique_key, i.indisprimary AS primary_key + FROM pg_class bc, pg_class ic, pg_index i, pg_attribute a WHERE bc.oid = i.indrelid AND ic.oid = i.indexrelid AND (i.indkey[0] = a.attnum OR i.indkey[1] = a.attnum OR i.indkey[2] = a.attnum OR i.indkey[3] = a.attnum OR i.indkey[4] = a.attnum OR i.indkey[5] = a.attnum OR i.indkey[6] = a.attnum OR i.indkey[7] = a.attnum) AND a.attrelid = bc.oid AND bc.relname = '%s'"; + + var $hasAffectedRows = true; + var $hasLimit = false; // set to true for pgsql 7 only. support pgsql/mysql SELECT * FROM TABLE LIMIT 10 + // below suggested by Freek Dijkstra + var $true = 't'; // string that represents TRUE for a database + var $false = 'f'; // string that represents FALSE for a database + var $fmtDate = "'Y-m-d'"; // used by DBDate() as the default date format used by the database + var $fmtTimeStamp = "'Y-m-d G:i:s'"; // used by DBTimeStamp as the default timestamp fmt. + var $hasMoveFirst = true; + var $hasGenID = true; + var $_genIDSQL = "SELECT NEXTVAL('%s')"; + var $_genSeqSQL = "CREATE SEQUENCE %s START %s"; + var $_dropSeqSQL = "DROP SEQUENCE %s"; + var $metaDefaultsSQL = "SELECT d.adnum as num, d.adsrc as def from pg_attrdef d, pg_class c where d.adrelid=c.oid and c.relname='%s' order by d.adnum"; + var $random = 'random()'; /// random function + var $concat_operator='||'; + + function _init($parentDriver) + { + + $parentDriver->hasTransactions = false; ## <<< BUG IN PDO pgsql driver + $parentDriver->hasInsertID = true; + $parentDriver->_nestedSQL = true; + } + + function ServerInfo() + { + $arr['description'] = ADOConnection::GetOne("select version()"); + $arr['version'] = ADOConnection::_findvers($arr['description']); + return $arr; + } + + function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false,$secs2cache=0) + { + $offsetStr = ($offset >= 0) ? " OFFSET $offset" : ''; + $limitStr = ($nrows >= 0) ? " LIMIT $nrows" : ''; + if ($secs2cache) + $rs = $this->CacheExecute($secs2cache,$sql."$limitStr$offsetStr",$inputarr); + else + $rs = $this->Execute($sql."$limitStr$offsetStr",$inputarr); + + return $rs; + } + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + $info = $this->ServerInfo(); + if ($info['version'] >= 7.3) { + $this->metaTablesSQL = "select tablename,'T' from pg_tables where tablename not like 'pg\_%' + and schemaname not in ( 'pg_catalog','information_schema') + union + select viewname,'V' from pg_views where viewname not like 'pg\_%' and schemaname not in ( 'pg_catalog','information_schema') "; + } + if ($mask) { + $save = $this->metaTablesSQL; + $mask = $this->qstr(strtolower($mask)); + if ($info['version']>=7.3) + $this->metaTablesSQL = " +select tablename,'T' from pg_tables where tablename like $mask and schemaname not in ( 'pg_catalog','information_schema') + union +select viewname,'V' from pg_views where viewname like $mask and schemaname not in ( 'pg_catalog','information_schema') "; + else + $this->metaTablesSQL = " +select tablename,'T' from pg_tables where tablename like $mask + union +select viewname,'V' from pg_views where viewname like $mask"; + } + $ret = ADOConnection::MetaTables($ttype,$showSchema); + + if ($mask) { + $this->metaTablesSQL = $save; + } + return $ret; + } + + function MetaColumns($table,$normalize=true) + { + global $ADODB_FETCH_MODE; + + $schema = false; + $this->_findschema($table,$schema); + + if ($normalize) $table = strtolower($table); + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== false) $savem = $this->SetFetchMode(false); + + if ($schema) $rs = $this->Execute(sprintf($this->metaColumnsSQL1,$table,$table,$schema)); + else $rs = $this->Execute(sprintf($this->metaColumnsSQL,$table,$table)); + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + if ($rs === false) { + $false = false; + return $false; + } + if (!empty($this->metaKeySQL)) { + // If we want the primary keys, we have to issue a separate query + // Of course, a modified version of the metaColumnsSQL query using a + // LEFT JOIN would have been much more elegant, but postgres does + // not support OUTER JOINS. So here is the clumsy way. + + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + + $rskey = $this->Execute(sprintf($this->metaKeySQL,($table))); + // fetch all result in once for performance. + $keys = $rskey->GetArray(); + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + $rskey->Close(); + unset($rskey); + } + + $rsdefa = array(); + if (!empty($this->metaDefaultsSQL)) { + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $sql = sprintf($this->metaDefaultsSQL, ($table)); + $rsdef = $this->Execute($sql); + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + if ($rsdef) { + while (!$rsdef->EOF) { + $num = $rsdef->fields['num']; + $s = $rsdef->fields['def']; + if (strpos($s,'::')===false && substr($s, 0, 1) == "'") { /* quoted strings hack... for now... fixme */ + $s = substr($s, 1); + $s = substr($s, 0, strlen($s) - 1); + } + + $rsdefa[$num] = $s; + $rsdef->MoveNext(); + } + } else { + ADOConnection::outp( "==> SQL => " . $sql); + } + unset($rsdef); + } + + $retarr = array(); + while (!$rs->EOF) { + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; + $fld->type = $rs->fields[1]; + $fld->max_length = $rs->fields[2]; + if ($fld->max_length <= 0) $fld->max_length = $rs->fields[3]-4; + if ($fld->max_length <= 0) $fld->max_length = -1; + if ($fld->type == 'numeric') { + $fld->scale = $fld->max_length & 0xFFFF; + $fld->max_length >>= 16; + } + // dannym + // 5 hasdefault; 6 num-of-column + $fld->has_default = ($rs->fields[5] == 't'); + if ($fld->has_default) { + $fld->default_value = $rsdefa[$rs->fields[6]]; + } + + //Freek + if ($rs->fields[4] == $this->true) { + $fld->not_null = true; + } + + // Freek + if (is_array($keys)) { + foreach($keys as $key) { + if ($fld->name == $key['column_name'] AND $key['primary_key'] == $this->true) + $fld->primary_key = true; + if ($fld->name == $key['column_name'] AND $key['unique_key'] == $this->true) + $fld->unique = true; // What name is more compatible? + } + } + + if ($ADODB_FETCH_MODE == ADODB_FETCH_NUM) $retarr[] = $fld; + else $retarr[($normalize) ? strtoupper($fld->name) : $fld->name] = $fld; + + $rs->MoveNext(); + } + $rs->Close(); + if (empty($retarr)) { + $false = false; + return $false; + } else return $retarr; + + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-pdo_sqlite.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_sqlite.inc.php new file mode 100644 index 000000000..d42c5191c --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_sqlite.inc.php @@ -0,0 +1,204 @@ +pdoDriver = $parentDriver; + $parentDriver->_bindInputArray = true; + $parentDriver->hasTransactions = false; // // should be set to false because of PDO SQLite driver not supporting changing autocommit mode + $parentDriver->hasInsertID = true; + } + + function ServerInfo() + { + $parent = $this->pdoDriver; + @($ver = array_pop($parent->GetCol("SELECT sqlite_version()"))); + @($enc = array_pop($parent->GetCol("PRAGMA encoding"))); + + $arr['version'] = $ver; + $arr['description'] = 'SQLite '; + $arr['encoding'] = $enc; + + return $arr; + } + + function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false,$secs2cache=0) + { + $parent = $this->pdoDriver; + $offsetStr = ($offset >= 0) ? " OFFSET $offset" : ''; + $limitStr = ($nrows >= 0) ? " LIMIT $nrows" : ($offset >= 0 ? ' LIMIT 999999999' : ''); + if ($secs2cache) + $rs = $parent->CacheExecute($secs2cache,$sql."$limitStr$offsetStr",$inputarr); + else + $rs = $parent->Execute($sql."$limitStr$offsetStr",$inputarr); + + return $rs; + } + + function GenID($seq='adodbseq',$start=1) + { + $parent = $this->pdoDriver; + // if you have to modify the parameter below, your database is overloaded, + // or you need to implement generation of id's yourself! + $MAXLOOPS = 100; + while (--$MAXLOOPS>=0) { + @($num = array_pop($parent->GetCol("SELECT id FROM {$seq}"))); + if ($num === false || !is_numeric($num)) { + @$parent->Execute(sprintf($this->_genSeqSQL ,$seq)); + $start -= 1; + $num = '0'; + $cnt = $parent->GetOne(sprintf($this->_genSeqCountSQL,$seq)); + if (!$cnt) { + $ok = $parent->Execute(sprintf($this->_genSeq2SQL,$seq,$start)); + } + if (!$ok) return false; + } + $parent->Execute(sprintf($this->_genIDSQL,$seq,$num)); + + if ($parent->affected_rows() > 0) { + $num += 1; + $parent->genID = intval($num); + return intval($num); + } + } + if ($fn = $parent->raiseErrorFn) { + $fn($parent->databaseType,'GENID',-32000,"Unable to generate unique id after $MAXLOOPS attempts",$seq,$num); + } + return false; + } + + function CreateSequence($seqname='adodbseq',$start=1) + { + $parent = $this->pdoDriver; + $ok = $parent->Execute(sprintf($this->_genSeqSQL,$seqname)); + if (!$ok) return false; + $start -= 1; + return $parent->Execute("insert into $seqname values($start)"); + } + + function SetTransactionMode($transaction_mode) + { + $parent = $this->pdoDriver; + $parent->_transmode = strtoupper($transaction_mode); + } + + function BeginTrans() + { + $parent = $this->pdoDriver; + if ($parent->transOff) return true; + $parent->transCnt += 1; + $parent->_autocommit = false; + return $parent->Execute("BEGIN {$parent->_transmode}"); + } + + function CommitTrans($ok=true) + { + $parent = $this->pdoDriver; + if ($parent->transOff) return true; + if (!$ok) return $parent->RollbackTrans(); + if ($parent->transCnt) $parent->transCnt -= 1; + $parent->_autocommit = true; + + $ret = $parent->Execute('COMMIT'); + return $ret; + } + + function RollbackTrans() + { + $parent = $this->pdoDriver; + if ($parent->transOff) return true; + if ($parent->transCnt) $parent->transCnt -= 1; + $parent->_autocommit = true; + + $ret = $parent->Execute('ROLLBACK'); + return $ret; + } + + + // mark newnham + function MetaColumns($tab,$normalize=true) + { + global $ADODB_FETCH_MODE; + + $parent = $this->pdoDriver; + $false = false; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + if ($parent->fetchMode !== false) $savem = $parent->SetFetchMode(false); + $rs = $parent->Execute("PRAGMA table_info('$tab')"); + if (isset($savem)) $parent->SetFetchMode($savem); + if (!$rs) { + $ADODB_FETCH_MODE = $save; + return $false; + } + $arr = array(); + while ($r = $rs->FetchRow()) { + $type = explode('(',$r['type']); + $size = ''; + if (sizeof($type)==2) + $size = trim($type[1],')'); + $fn = strtoupper($r['name']); + $fld = new ADOFieldObject; + $fld->name = $r['name']; + $fld->type = $type[0]; + $fld->max_length = $size; + $fld->not_null = $r['notnull']; + $fld->primary_key = $r['pk']; + $fld->default_value = $r['dflt_value']; + $fld->scale = 0; + if ($save == ADODB_FETCH_NUM) $arr[] = $fld; + else $arr[strtoupper($fld->name)] = $fld; + } + $rs->Close(); + $ADODB_FETCH_MODE = $save; + return $arr; + } + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + $parent = $this->pdoDriver; + + if ($mask) { + $save = $this->metaTablesSQL; + $mask = $this->qstr(strtoupper($mask)); + $this->metaTablesSQL .= " AND name LIKE $mask"; + } + + $ret = $parent->GetCol($this->metaTablesSQL); + + if ($mask) { + $this->metaTablesSQL = $save; + } + return $ret; + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-pdo_sqlsrv.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_sqlsrv.inc.php new file mode 100644 index 000000000..869e8e181 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-pdo_sqlsrv.inc.php @@ -0,0 +1,49 @@ +hasTransactions = true; + $parentDriver->_bindInputArray = true; + $parentDriver->hasInsertID = true; + $parentDriver->fmtTimeStamp = "'Y-m-d H:i:s'"; + $parentDriver->fmtDate = "'Y-m-d'"; + } + + function BeginTrans() + { + $returnval = parent::BeginTrans(); + return $returnval; + } + + function MetaColumns($table, $normalize = true) + { + return false; + } + + function MetaTables($ttype = false, $showSchema = false, $mask = false) + { + return false; + } + + function SelectLimit($sql, $nrows = -1, $offset = -1, $inputarr = false, $secs2cache = 0) + { + $ret = ADOConnection::SelectLimit($sql, $nrows, $offset, $inputarr, $secs2cache); + return $ret; + } + + function ServerInfo() + { + return ADOConnection::ServerInfo(); + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-postgres.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-postgres.inc.php new file mode 100644 index 000000000..3c866b72f --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-postgres.inc.php @@ -0,0 +1,14 @@ + + jlim - changed concat operator to || and data types to MetaType to match documented pgsql types + see http://www.postgresql.org/devel-corner/docs/postgres/datatype.htm + 22 Nov 2000 jlim - added changes to FetchField() and MetaTables() contributed by "raser" + 27 Nov 2000 jlim - added changes to _connect/_pconnect from ideas by "Lennie" + 15 Dec 2000 jlim - added changes suggested by Additional code changes by "Eric G. Werk" egw@netguide.dk. + 31 Jan 2002 jlim - finally installed postgresql. testing + 01 Mar 2001 jlim - Freek Dijkstra changes, also support for text type + + See http://www.varlena.com/varlena/GeneralBits/47.php + + -- What indexes are on my table? + select * from pg_indexes where tablename = 'tablename'; + + -- What triggers are on my table? + select c.relname as "Table", t.tgname as "Trigger Name", + t.tgconstrname as "Constraint Name", t.tgenabled as "Enabled", + t.tgisconstraint as "Is Constraint", cc.relname as "Referenced Table", + p.proname as "Function Name" + from pg_trigger t, pg_class c, pg_class cc, pg_proc p + where t.tgfoid = p.oid and t.tgrelid = c.oid + and t.tgconstrrelid = cc.oid + and c.relname = 'tablename'; + + -- What constraints are on my table? + select r.relname as "Table", c.conname as "Constraint Name", + contype as "Constraint Type", conkey as "Key Columns", + confkey as "Foreign Columns", consrc as "Source" + from pg_class r, pg_constraint c + where r.oid = c.conrelid + and relname = 'tablename'; + +*/ + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +function adodb_addslashes($s) +{ + $len = strlen($s); + if ($len == 0) return "''"; + if (strncmp($s,"'",1) === 0 && substr($s,$len-1) == "'") return $s; // already quoted + + return "'".addslashes($s)."'"; +} + +class ADODB_postgres64 extends ADOConnection{ + var $databaseType = 'postgres64'; + var $dataProvider = 'postgres'; + var $hasInsertID = true; + var $_resultid = false; + var $concat_operator='||'; + var $metaDatabasesSQL = "select datname from pg_database where datname not in ('template0','template1') order by 1"; + var $metaTablesSQL = "select tablename,'T' from pg_tables where tablename not like 'pg\_%' + and tablename not in ('sql_features', 'sql_implementation_info', 'sql_languages', + 'sql_packages', 'sql_sizing', 'sql_sizing_profiles') + union + select viewname,'V' from pg_views where viewname not like 'pg\_%'"; + //"select tablename from pg_tables where tablename not like 'pg_%' order by 1"; + var $isoDates = true; // accepts dates in ISO format + var $sysDate = "CURRENT_DATE"; + var $sysTimeStamp = "CURRENT_TIMESTAMP"; + var $blobEncodeType = 'C'; + var $metaColumnsSQL = "SELECT a.attname,t.typname,a.attlen,a.atttypmod,a.attnotnull,a.atthasdef,a.attnum + FROM pg_class c, pg_attribute a,pg_type t + WHERE relkind in ('r','v') AND (c.relname='%s' or c.relname = lower('%s')) and a.attname not like '....%%' + AND a.attnum > 0 AND a.atttypid = t.oid AND a.attrelid = c.oid ORDER BY a.attnum"; + + // used when schema defined + var $metaColumnsSQL1 = "SELECT a.attname, t.typname, a.attlen, a.atttypmod, a.attnotnull, a.atthasdef, a.attnum + FROM pg_class c, pg_attribute a, pg_type t, pg_namespace n + WHERE relkind in ('r','v') AND (c.relname='%s' or c.relname = lower('%s')) + and c.relnamespace=n.oid and n.nspname='%s' + and a.attname not like '....%%' AND a.attnum > 0 + AND a.atttypid = t.oid AND a.attrelid = c.oid ORDER BY a.attnum"; + + // get primary key etc -- from Freek Dijkstra + var $metaKeySQL = "SELECT ic.relname AS index_name, a.attname AS column_name,i.indisunique AS unique_key, i.indisprimary AS primary_key + FROM pg_class bc, pg_class ic, pg_index i, pg_attribute a + WHERE bc.oid = i.indrelid AND ic.oid = i.indexrelid + AND (i.indkey[0] = a.attnum OR i.indkey[1] = a.attnum OR i.indkey[2] = a.attnum OR i.indkey[3] = a.attnum OR i.indkey[4] = a.attnum OR i.indkey[5] = a.attnum OR i.indkey[6] = a.attnum OR i.indkey[7] = a.attnum) + AND a.attrelid = bc.oid AND bc.relname = '%s'"; + + var $hasAffectedRows = true; + var $hasLimit = false; // set to true for pgsql 7 only. support pgsql/mysql SELECT * FROM TABLE LIMIT 10 + // below suggested by Freek Dijkstra + var $true = 'TRUE'; // string that represents TRUE for a database + var $false = 'FALSE'; // string that represents FALSE for a database + var $fmtDate = "'Y-m-d'"; // used by DBDate() as the default date format used by the database + var $fmtTimeStamp = "'Y-m-d H:i:s'"; // used by DBTimeStamp as the default timestamp fmt. + var $hasMoveFirst = true; + var $hasGenID = true; + var $_genIDSQL = "SELECT NEXTVAL('%s')"; + var $_genSeqSQL = "CREATE SEQUENCE %s START %s"; + var $_dropSeqSQL = "DROP SEQUENCE %s"; + var $metaDefaultsSQL = "SELECT d.adnum as num, d.adsrc as def from pg_attrdef d, pg_class c where d.adrelid=c.oid and c.relname='%s' order by d.adnum"; + var $random = 'random()'; /// random function + var $autoRollback = true; // apparently pgsql does not autorollback properly before php 4.3.4 + // http://bugs.php.net/bug.php?id=25404 + + var $uniqueIisR = true; + var $_bindInputArray = false; // requires postgresql 7.3+ and ability to modify database + var $disableBlobs = false; // set to true to disable blob checking, resulting in 2-5% improvement in performance. + + var $_pnum = 0; + + // The last (fmtTimeStamp is not entirely correct: + // PostgreSQL also has support for time zones, + // and writes these time in this format: "2001-03-01 18:59:26+02". + // There is no code for the "+02" time zone information, so I just left that out. + // I'm not familiar enough with both ADODB as well as Postgres + // to know what the concequences are. The other values are correct (wheren't in 0.94) + // -- Freek Dijkstra + + function __construct() + { + // changes the metaColumnsSQL, adds columns: attnum[6] + } + + function ServerInfo() + { + if (isset($this->version)) return $this->version; + + $arr['description'] = $this->GetOne("select version()"); + $arr['version'] = ADOConnection::_findvers($arr['description']); + $this->version = $arr; + return $arr; + } + + function IfNull( $field, $ifNull ) + { + return " coalesce($field, $ifNull) "; + } + + // get the last id - never tested + function pg_insert_id($tablename,$fieldname) + { + $result=pg_query($this->_connectionID, 'SELECT last_value FROM '. $tablename .'_'. $fieldname .'_seq'); + if ($result) { + $arr = @pg_fetch_row($result,0); + pg_free_result($result); + if (isset($arr[0])) return $arr[0]; + } + return false; + } + + /** + * Warning from http://www.php.net/manual/function.pg-getlastoid.php: + * Using a OID as a unique identifier is not generally wise. + * Unless you are very careful, you might end up with a tuple having + * a different OID if a database must be reloaded. + */ + function _insertid($table,$column) + { + if (!is_resource($this->_resultid) || get_resource_type($this->_resultid) !== 'pgsql result') return false; + $oid = pg_getlastoid($this->_resultid); + // to really return the id, we need the table and column-name, else we can only return the oid != id + return empty($table) || empty($column) ? $oid : $this->GetOne("SELECT $column FROM $table WHERE oid=".(int)$oid); + } + + function _affectedrows() + { + if (!is_resource($this->_resultid) || get_resource_type($this->_resultid) !== 'pgsql result') return false; + return pg_affected_rows($this->_resultid); + } + + + /** + * @return true/false + */ + function BeginTrans() + { + if ($this->transOff) return true; + $this->transCnt += 1; + return pg_query($this->_connectionID, 'begin '.$this->_transmode); + } + + function RowLock($tables,$where,$col='1 as adodbignore') + { + if (!$this->transCnt) $this->BeginTrans(); + return $this->GetOne("select $col from $tables where $where for update"); + } + + // returns true/false. + function CommitTrans($ok=true) + { + if ($this->transOff) return true; + if (!$ok) return $this->RollbackTrans(); + + $this->transCnt -= 1; + return pg_query($this->_connectionID, 'commit'); + } + + // returns true/false + function RollbackTrans() + { + if ($this->transOff) return true; + $this->transCnt -= 1; + return pg_query($this->_connectionID, 'rollback'); + } + + function MetaTables($ttype=false,$showSchema=false,$mask=false) + { + $info = $this->ServerInfo(); + if ($info['version'] >= 7.3) { + $this->metaTablesSQL = " + select table_name,'T' from information_schema.tables where table_schema not in ( 'pg_catalog','information_schema') + union + select table_name,'V' from information_schema.views where table_schema not in ( 'pg_catalog','information_schema') "; + } + if ($mask) { + $save = $this->metaTablesSQL; + $mask = $this->qstr(strtolower($mask)); + if ($info['version']>=7.3) + $this->metaTablesSQL = " + select table_name,'T' from information_schema.tables where table_name like $mask and table_schema not in ( 'pg_catalog','information_schema') + union + select table_name,'V' from information_schema.views where table_name like $mask and table_schema not in ( 'pg_catalog','information_schema') "; + else + $this->metaTablesSQL = " + select tablename,'T' from pg_tables where tablename like $mask + union + select viewname,'V' from pg_views where viewname like $mask"; + } + $ret = ADOConnection::MetaTables($ttype,$showSchema); + + if ($mask) { + $this->metaTablesSQL = $save; + } + return $ret; + } + + + // if magic quotes disabled, use pg_escape_string() + function qstr($s,$magic_quotes=false) + { + if (is_bool($s)) return $s ? 'true' : 'false'; + + if (!$magic_quotes) { + if (ADODB_PHPVER >= 0x5200 && $this->_connectionID) { + return "'".pg_escape_string($this->_connectionID,$s)."'"; + } + if (ADODB_PHPVER >= 0x4200) { + return "'".pg_escape_string($s)."'"; + } + if ($this->replaceQuote[0] == '\\'){ + $s = adodb_str_replace(array('\\',"\0"),array('\\\\',"\\\\000"),$s); + } + return "'".str_replace("'",$this->replaceQuote,$s)."'"; + } + + // undo magic quotes for " + $s = str_replace('\\"','"',$s); + return "'$s'"; + } + + + + // Format date column in sql string given an input format that understands Y M D + function SQLDate($fmt, $col=false) + { + if (!$col) $col = $this->sysTimeStamp; + $s = 'TO_CHAR('.$col.",'"; + + $len = strlen($fmt); + for ($i=0; $i < $len; $i++) { + $ch = $fmt[$i]; + switch($ch) { + case 'Y': + case 'y': + $s .= 'YYYY'; + break; + case 'Q': + case 'q': + $s .= 'Q'; + break; + + case 'M': + $s .= 'Mon'; + break; + + case 'm': + $s .= 'MM'; + break; + case 'D': + case 'd': + $s .= 'DD'; + break; + + case 'H': + $s.= 'HH24'; + break; + + case 'h': + $s .= 'HH'; + break; + + case 'i': + $s .= 'MI'; + break; + + case 's': + $s .= 'SS'; + break; + + case 'a': + case 'A': + $s .= 'AM'; + break; + + case 'w': + $s .= 'D'; + break; + + case 'l': + $s .= 'DAY'; + break; + + case 'W': + $s .= 'WW'; + break; + + default: + // handle escape characters... + if ($ch == '\\') { + $i++; + $ch = substr($fmt,$i,1); + } + if (strpos('-/.:;, ',$ch) !== false) $s .= $ch; + else $s .= '"'.$ch.'"'; + + } + } + return $s. "')"; + } + + + + /* + * Load a Large Object from a file + * - the procedure stores the object id in the table and imports the object using + * postgres proprietary blob handling routines + * + * contributed by Mattia Rossi mattia@technologist.com + * modified for safe mode by juraj chlebec + */ + function UpdateBlobFile($table,$column,$path,$where,$blobtype='BLOB') + { + pg_query($this->_connectionID, 'begin'); + + $fd = fopen($path,'r'); + $contents = fread($fd,filesize($path)); + fclose($fd); + + $oid = pg_lo_create($this->_connectionID); + $handle = pg_lo_open($this->_connectionID, $oid, 'w'); + pg_lo_write($handle, $contents); + pg_lo_close($handle); + + // $oid = pg_lo_import ($path); + pg_query($this->_connectionID, 'commit'); + $rs = ADOConnection::UpdateBlob($table,$column,$oid,$where,$blobtype); + $rez = !empty($rs); + return $rez; + } + + /* + * Deletes/Unlinks a Blob from the database, otherwise it + * will be left behind + * + * Returns TRUE on success or FALSE on failure. + * + * contributed by Todd Rogers todd#windfox.net + */ + function BlobDelete( $blob ) + { + pg_query($this->_connectionID, 'begin'); + $result = @pg_lo_unlink($blob); + pg_query($this->_connectionID, 'commit'); + return( $result ); + } + + /* + Hueristic - not guaranteed to work. + */ + function GuessOID($oid) + { + if (strlen($oid)>16) return false; + return is_numeric($oid); + } + + /* + * If an OID is detected, then we use pg_lo_* to open the oid file and read the + * real blob from the db using the oid supplied as a parameter. If you are storing + * blobs using bytea, we autodetect and process it so this function is not needed. + * + * contributed by Mattia Rossi mattia@technologist.com + * + * see http://www.postgresql.org/idocs/index.php?largeobjects.html + * + * Since adodb 4.54, this returns the blob, instead of sending it to stdout. Also + * added maxsize parameter, which defaults to $db->maxblobsize if not defined. + */ + function BlobDecode($blob,$maxsize=false,$hastrans=true) + { + if (!$this->GuessOID($blob)) return $blob; + + if ($hastrans) pg_query($this->_connectionID,'begin'); + $fd = @pg_lo_open($this->_connectionID,$blob,'r'); + if ($fd === false) { + if ($hastrans) pg_query($this->_connectionID,'commit'); + return $blob; + } + if (!$maxsize) $maxsize = $this->maxblobsize; + $realblob = @pg_lo_read($fd,$maxsize); + @pg_loclose($fd); + if ($hastrans) pg_query($this->_connectionID,'commit'); + return $realblob; + } + + /* + See http://www.postgresql.org/idocs/index.php?datatype-binary.html + + NOTE: SQL string literals (input strings) must be preceded with two backslashes + due to the fact that they must pass through two parsers in the PostgreSQL + backend. + */ + function BlobEncode($blob) + { + if (ADODB_PHPVER >= 0x5200) return pg_escape_bytea($this->_connectionID, $blob); + if (ADODB_PHPVER >= 0x4200) return pg_escape_bytea($blob); + + /*92=backslash, 0=null, 39=single-quote*/ + $badch = array(chr(92),chr(0),chr(39)); # \ null ' + $fixch = array('\\\\134','\\\\000','\\\\047'); + return adodb_str_replace($badch,$fixch,$blob); + + // note that there is a pg_escape_bytea function only for php 4.2.0 or later + } + + // assumes bytea for blob, and varchar for clob + function UpdateBlob($table,$column,$val,$where,$blobtype='BLOB') + { + if ($blobtype == 'CLOB') { + return $this->Execute("UPDATE $table SET $column=" . $this->qstr($val) . " WHERE $where"); + } + // do not use bind params which uses qstr(), as blobencode() already quotes data + return $this->Execute("UPDATE $table SET $column='".$this->BlobEncode($val)."'::bytea WHERE $where"); + } + + function OffsetDate($dayFraction,$date=false) + { + if (!$date) $date = $this->sysDate; + else if (strncmp($date,"'",1) == 0) { + $len = strlen($date); + if (10 <= $len && $len <= 12) $date = 'date '.$date; + else $date = 'timestamp '.$date; + } + + + return "($date+interval'".($dayFraction * 1440)." minutes')"; + #return "($date+interval'$dayFraction days')"; + } + + /** + * Generate the SQL to retrieve MetaColumns data + * @param string $table Table name + * @param string $schema Schema name (can be blank) + * @return string SQL statement to execute + */ + protected function _generateMetaColumnsSQL($table, $schema) + { + if ($schema) { + return sprintf($this->metaColumnsSQL1, $table, $table, $schema); + } + else { + return sprintf($this->metaColumnsSQL, $table, $table, $schema); + } + } + + // for schema support, pass in the $table param "$schema.$tabname". + // converts field names to lowercase, $upper is ignored + // see http://phplens.com/lens/lensforum/msgs.php?id=14018 for more info + function MetaColumns($table,$normalize=true) + { + global $ADODB_FETCH_MODE; + + $schema = false; + $false = false; + $this->_findschema($table,$schema); + + if ($normalize) $table = strtolower($table); + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== false) $savem = $this->SetFetchMode(false); + + $rs = $this->Execute($this->_generateMetaColumnsSQL($table, $schema)); + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + if ($rs === false) { + return $false; + } + if (!empty($this->metaKeySQL)) { + // If we want the primary keys, we have to issue a separate query + // Of course, a modified version of the metaColumnsSQL query using a + // LEFT JOIN would have been much more elegant, but postgres does + // not support OUTER JOINS. So here is the clumsy way. + + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + + $rskey = $this->Execute(sprintf($this->metaKeySQL,($table))); + // fetch all result in once for performance. + $keys = $rskey->GetArray(); + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + $rskey->Close(); + unset($rskey); + } + + $rsdefa = array(); + if (!empty($this->metaDefaultsSQL)) { + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $sql = sprintf($this->metaDefaultsSQL, ($table)); + $rsdef = $this->Execute($sql); + if (isset($savem)) $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + if ($rsdef) { + while (!$rsdef->EOF) { + $num = $rsdef->fields['num']; + $s = $rsdef->fields['def']; + if (strpos($s,'::')===false && substr($s, 0, 1) == "'") { /* quoted strings hack... for now... fixme */ + $s = substr($s, 1); + $s = substr($s, 0, strlen($s) - 1); + } + + $rsdefa[$num] = $s; + $rsdef->MoveNext(); + } + } else { + ADOConnection::outp( "==> SQL => " . $sql); + } + unset($rsdef); + } + + $retarr = array(); + while (!$rs->EOF) { + $fld = new ADOFieldObject(); + $fld->name = $rs->fields[0]; + $fld->type = $rs->fields[1]; + $fld->max_length = $rs->fields[2]; + $fld->attnum = $rs->fields[6]; + + if ($fld->max_length <= 0) $fld->max_length = $rs->fields[3]-4; + if ($fld->max_length <= 0) $fld->max_length = -1; + if ($fld->type == 'numeric') { + $fld->scale = $fld->max_length & 0xFFFF; + $fld->max_length >>= 16; + } + // dannym + // 5 hasdefault; 6 num-of-column + $fld->has_default = ($rs->fields[5] == 't'); + if ($fld->has_default) { + $fld->default_value = $rsdefa[$rs->fields[6]]; + } + + //Freek + $fld->not_null = $rs->fields[4] == 't'; + + + // Freek + if (is_array($keys)) { + foreach($keys as $key) { + if ($fld->name == $key['column_name'] AND $key['primary_key'] == 't') + $fld->primary_key = true; + if ($fld->name == $key['column_name'] AND $key['unique_key'] == 't') + $fld->unique = true; // What name is more compatible? + } + } + + if ($ADODB_FETCH_MODE == ADODB_FETCH_NUM) $retarr[] = $fld; + else $retarr[($normalize) ? strtoupper($fld->name) : $fld->name] = $fld; + + $rs->MoveNext(); + } + $rs->Close(); + if (empty($retarr)) + return $false; + else + return $retarr; + + } + + function Param($name,$type='C') + { + if ($name) { + $this->_pnum += 1; + } else { + // Reset param num if $name is false + $this->_pnum = 1; + } + return '$'.$this->_pnum; + } + + function MetaIndexes ($table, $primary = FALSE, $owner = false) + { + global $ADODB_FETCH_MODE; + + $schema = false; + $this->_findschema($table,$schema); + + if ($schema) { // requires pgsql 7.3+ - pg_namespace used. + $sql = ' + SELECT c.relname as "Name", i.indisunique as "Unique", i.indkey as "Columns" + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_index i ON i.indexrelid=c.oid + JOIN pg_catalog.pg_class c2 ON c2.oid=i.indrelid + ,pg_namespace n + WHERE (c2.relname=\'%s\' or c2.relname=lower(\'%s\')) + and c.relnamespace=c2.relnamespace + and c.relnamespace=n.oid + and n.nspname=\'%s\''; + } else { + $sql = ' + SELECT c.relname as "Name", i.indisunique as "Unique", i.indkey as "Columns" + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_index i ON i.indexrelid=c.oid + JOIN pg_catalog.pg_class c2 ON c2.oid=i.indrelid + WHERE (c2.relname=\'%s\' or c2.relname=lower(\'%s\'))'; + } + + if ($primary == FALSE) { + $sql .= ' AND i.indisprimary=false;'; + } + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + + $rs = $this->Execute(sprintf($sql,$table,$table,$schema)); + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + if (!is_object($rs)) { + $false = false; + return $false; + } + + $col_names = $this->MetaColumnNames($table,true,true); + //3rd param is use attnum, + // see http://sourceforge.net/tracker/index.php?func=detail&aid=1451245&group_id=42718&atid=433976 + $indexes = array(); + while ($row = $rs->FetchRow()) { + $columns = array(); + foreach (explode(' ', $row[2]) as $col) { + $columns[] = $col_names[$col]; + } + + $indexes[$row[0]] = array( + 'unique' => ($row[1] == 't'), + 'columns' => $columns + ); + } + return $indexes; + } + + // returns true or false + // + // examples: + // $db->Connect("host=host1 user=user1 password=secret port=4341"); + // $db->Connect('host1','user1','secret'); + function _connect($str,$user='',$pwd='',$db='',$ctype=0) + { + if (!function_exists('pg_connect')) return null; + + $this->_errorMsg = false; + + if ($user || $pwd || $db) { + $user = adodb_addslashes($user); + $pwd = adodb_addslashes($pwd); + if (strlen($db) == 0) $db = 'template1'; + $db = adodb_addslashes($db); + if ($str) { + $host = explode(":", $str); + if ($host[0]) $str = "host=".adodb_addslashes($host[0]); + else $str = ''; + if (isset($host[1])) $str .= " port=$host[1]"; + else if (!empty($this->port)) $str .= " port=".$this->port; + } + if ($user) $str .= " user=".$user; + if ($pwd) $str .= " password=".$pwd; + if ($db) $str .= " dbname=".$db; + } + + //if ($user) $linea = "user=$user host=$linea password=$pwd dbname=$db port=5432"; + + if ($ctype === 1) { // persistent + $this->_connectionID = pg_pconnect($str); + } else { + if ($ctype === -1) { // nconnect, we trick pgsql ext by changing the connection str + static $ncnt; + + if (empty($ncnt)) $ncnt = 1; + else $ncnt += 1; + + $str .= str_repeat(' ',$ncnt); + } + $this->_connectionID = pg_connect($str); + } + if ($this->_connectionID === false) return false; + $this->Execute("set datestyle='ISO'"); + + $info = $this->ServerInfo(); + $this->pgVersion = (float) substr($info['version'],0,3); + if ($this->pgVersion >= 7.1) { // good till version 999 + $this->_nestedSQL = true; + } + + # PostgreSQL 9.0 changed the default output for bytea from 'escape' to 'hex' + # PHP does not handle 'hex' properly ('x74657374' is returned as 't657374') + # https://bugs.php.net/bug.php?id=59831 states this is in fact not a bug, + # so we manually set bytea_output + if ( !empty($this->connection->noBlobs) && version_compare($info['version'], '9.0', '>=')) { + $this->Execute('set bytea_output=escape'); + } + + return true; + } + + function _nconnect($argHostname, $argUsername, $argPassword, $argDatabaseName) + { + return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabaseName,-1); + } + + // returns true or false + // + // examples: + // $db->PConnect("host=host1 user=user1 password=secret port=4341"); + // $db->PConnect('host1','user1','secret'); + function _pconnect($str,$user='',$pwd='',$db='') + { + return $this->_connect($str,$user,$pwd,$db,1); + } + + + // returns queryID or false + function _query($sql,$inputarr=false) + { + $this->_pnum = 0; + $this->_errorMsg = false; + if ($inputarr) { + /* + It appears that PREPARE/EXECUTE is slower for many queries. + + For query executed 1000 times: + "select id,firstname,lastname from adoxyz + where firstname not like ? and lastname not like ? and id = ?" + + with plan = 1.51861286163 secs + no plan = 1.26903700829 secs + */ + $plan = 'P'.md5($sql); + + $execp = ''; + foreach($inputarr as $v) { + if ($execp) $execp .= ','; + if (is_string($v)) { + if (strncmp($v,"'",1) !== 0) $execp .= $this->qstr($v); + } else { + $execp .= $v; + } + } + + if ($execp) $exsql = "EXECUTE $plan ($execp)"; + else $exsql = "EXECUTE $plan"; + + + $rez = @pg_execute($this->_connectionID,$exsql); + if (!$rez) { + # Perhaps plan does not exist? Prepare/compile plan. + $params = ''; + foreach($inputarr as $v) { + if ($params) $params .= ','; + if (is_string($v)) { + $params .= 'VARCHAR'; + } else if (is_integer($v)) { + $params .= 'INTEGER'; + } else { + $params .= "REAL"; + } + } + $sqlarr = explode('?',$sql); + //print_r($sqlarr); + $sql = ''; + $i = 1; + foreach($sqlarr as $v) { + $sql .= $v.' $'.$i; + $i++; + } + $s = "PREPARE $plan ($params) AS ".substr($sql,0,strlen($sql)-2); + //adodb_pr($s); + $rez = pg_execute($this->_connectionID,$s); + //echo $this->ErrorMsg(); + } + if ($rez) + $rez = pg_execute($this->_connectionID,$exsql); + } else { + //adodb_backtrace(); + $rez = pg_query($this->_connectionID,$sql); + } + // check if no data returned, then no need to create real recordset + if ($rez && pg_num_fields($rez) <= 0) { + if (is_resource($this->_resultid) && get_resource_type($this->_resultid) === 'pgsql result') { + pg_free_result($this->_resultid); + } + $this->_resultid = $rez; + return true; + } + + return $rez; + } + + function _errconnect() + { + if (defined('DB_ERROR_CONNECT_FAILED')) return DB_ERROR_CONNECT_FAILED; + else return 'Database connection failed'; + } + + /* Returns: the last error message from previous database operation */ + function ErrorMsg() + { + if ($this->_errorMsg !== false) return $this->_errorMsg; + if (ADODB_PHPVER >= 0x4300) { + if (!empty($this->_resultid)) { + $this->_errorMsg = @pg_result_error($this->_resultid); + if ($this->_errorMsg) return $this->_errorMsg; + } + + if (!empty($this->_connectionID)) { + $this->_errorMsg = @pg_last_error($this->_connectionID); + } else $this->_errorMsg = $this->_errconnect(); + } else { + if (empty($this->_connectionID)) $this->_errconnect(); + else $this->_errorMsg = @pg_errormessage($this->_connectionID); + } + return $this->_errorMsg; + } + + function ErrorNo() + { + $e = $this->ErrorMsg(); + if (strlen($e)) { + return ADOConnection::MetaError($e); + } + return 0; + } + + // returns true or false + function _close() + { + if ($this->transCnt) $this->RollbackTrans(); + if ($this->_resultid) { + @pg_free_result($this->_resultid); + $this->_resultid = false; + } + @pg_close($this->_connectionID); + $this->_connectionID = false; + return true; + } + + + /* + * Maximum size of C field + */ + function CharMax() + { + return 1000000000; // should be 1 Gb? + } + + /* + * Maximum size of X field + */ + function TextMax() + { + return 1000000000; // should be 1 Gb? + } + + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordSet_postgres64 extends ADORecordSet{ + var $_blobArr; + var $databaseType = "postgres64"; + var $canSeek = true; + + function __construct($queryID, $mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + switch ($mode) + { + case ADODB_FETCH_NUM: $this->fetchMode = PGSQL_NUM; break; + case ADODB_FETCH_ASSOC:$this->fetchMode = PGSQL_ASSOC; break; + + case ADODB_FETCH_DEFAULT: + case ADODB_FETCH_BOTH: + default: $this->fetchMode = PGSQL_BOTH; break; + } + $this->adodbFetchMode = $mode; + + // Parent's constructor + parent::__construct($queryID); + } + + function GetRowAssoc($upper = ADODB_ASSOC_CASE) + { + if ($this->fetchMode == PGSQL_ASSOC && $upper == ADODB_ASSOC_CASE_LOWER) { + return $this->fields; + } + $row = ADORecordSet::GetRowAssoc($upper); + return $row; + } + + + function _initrs() + { + global $ADODB_COUNTRECS; + $qid = $this->_queryID; + $this->_numOfRows = ($ADODB_COUNTRECS)? @pg_num_rows($qid):-1; + $this->_numOfFields = @pg_num_fields($qid); + + // cache types for blob decode check + // apparently pg_field_type actually performs an sql query on the database to get the type. + if (empty($this->connection->noBlobs)) + for ($i=0, $max = $this->_numOfFields; $i < $max; $i++) { + if (pg_field_type($qid,$i) == 'bytea') { + $this->_blobArr[$i] = pg_field_name($qid,$i); + } + } + } + + /* Use associative array to get fields array */ + function Fields($colname) + { + if ($this->fetchMode != PGSQL_NUM) return @$this->fields[$colname]; + + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + function FetchField($off = 0) + { + // offsets begin at 0 + + $o= new ADOFieldObject(); + $o->name = @pg_field_name($this->_queryID,$off); + $o->type = @pg_field_type($this->_queryID,$off); + $o->max_length = @pg_fieldsize($this->_queryID,$off); + return $o; + } + + function _seek($row) + { + return @pg_fetch_row($this->_queryID,$row); + } + + function _decode($blob) + { + if ($blob === NULL) return NULL; +// eval('$realblob="'.adodb_str_replace(array('"','$'),array('\"','\$'),$blob).'";'); + return pg_unescape_bytea($blob); + } + + function _fixblobs() + { + if ($this->fetchMode == PGSQL_NUM || $this->fetchMode == PGSQL_BOTH) { + foreach($this->_blobArr as $k => $v) { + $this->fields[$k] = ADORecordSet_postgres64::_decode($this->fields[$k]); + } + } + if ($this->fetchMode == PGSQL_ASSOC || $this->fetchMode == PGSQL_BOTH) { + foreach($this->_blobArr as $k => $v) { + $this->fields[$v] = ADORecordSet_postgres64::_decode($this->fields[$v]); + } + } + } + + // 10% speedup to move MoveNext to child class + function MoveNext() + { + if (!$this->EOF) { + $this->_currentRow++; + if ($this->_numOfRows < 0 || $this->_numOfRows > $this->_currentRow) { + $this->fields = @pg_fetch_array($this->_queryID,$this->_currentRow,$this->fetchMode); + if (is_array($this->fields) && $this->fields) { + if (isset($this->_blobArr)) $this->_fixblobs(); + return true; + } + } + $this->fields = false; + $this->EOF = true; + } + return false; + } + + function _fetch() + { + + if ($this->_currentRow >= $this->_numOfRows && $this->_numOfRows >= 0) + return false; + + $this->fields = @pg_fetch_array($this->_queryID,$this->_currentRow,$this->fetchMode); + + if ($this->fields && isset($this->_blobArr)) $this->_fixblobs(); + + return (is_array($this->fields)); + } + + function _close() + { + return @pg_free_result($this->_queryID); + } + + function MetaType($t,$len=-1,$fieldobj=false) + { + if (is_object($t)) { + $fieldobj = $t; + $t = $fieldobj->type; + $len = $fieldobj->max_length; + } + switch (strtoupper($t)) { + case 'MONEY': // stupid, postgres expects money to be a string + case 'INTERVAL': + case 'CHAR': + case 'CHARACTER': + case 'VARCHAR': + case 'NAME': + case 'BPCHAR': + case '_VARCHAR': + case 'INET': + case 'MACADDR': + if ($len <= $this->blobSize) return 'C'; + + case 'TEXT': + return 'X'; + + case 'IMAGE': // user defined type + case 'BLOB': // user defined type + case 'BIT': // This is a bit string, not a single bit, so don't return 'L' + case 'VARBIT': + case 'BYTEA': + return 'B'; + + case 'BOOL': + case 'BOOLEAN': + return 'L'; + + case 'DATE': + return 'D'; + + + case 'TIMESTAMP WITHOUT TIME ZONE': + case 'TIME': + case 'DATETIME': + case 'TIMESTAMP': + case 'TIMESTAMPTZ': + return 'T'; + + case 'SMALLINT': + case 'BIGINT': + case 'INTEGER': + case 'INT8': + case 'INT4': + case 'INT2': + if (isset($fieldobj) && + empty($fieldobj->primary_key) && (!$this->connection->uniqueIisR || empty($fieldobj->unique))) return 'I'; + + case 'OID': + case 'SERIAL': + return 'R'; + + default: + return 'N'; + } + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-postgres7.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-postgres7.inc.php new file mode 100644 index 000000000..ebdf2acfb --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-postgres7.inc.php @@ -0,0 +1,386 @@ + 0 + AND a.attrelid = c.oid + ORDER BY + a.attnum"; + + // used when schema defined + var $metaColumnsSQL1 = " + SELECT + a.attname, + CASE + WHEN x.sequence_name != '' + THEN 'SERIAL' + ELSE t.typname + END AS typname, + a.attlen, a.atttypmod, a.attnotnull, a.atthasdef, a.attnum + FROM + pg_class c, + pg_namespace n, + pg_attribute a + JOIN pg_type t ON a.atttypid = t.oid + LEFT JOIN ( + SELECT + c.relname as sequence_name, + c1.relname as related_table, + a.attname as related_column + FROM pg_class c + JOIN pg_depend d ON d.objid = c.oid + LEFT JOIN pg_class c1 ON d.refobjid = c1.oid + LEFT JOIN pg_attribute a ON (d.refobjid, d.refobjsubid) = (a.attrelid, a.attnum) + WHERE c.relkind = 'S' AND c1.relname = '%s' + ) x ON x.related_column= a.attname + WHERE + c.relkind in ('r','v') + AND (c.relname='%s' or c.relname = lower('%s')) + AND c.relnamespace=n.oid and n.nspname='%s' + AND a.attname not like '....%%' + AND a.attnum > 0 + AND a.atttypid = t.oid + AND a.attrelid = c.oid + ORDER BY a.attnum"; + + + function __construct() + { + parent::__construct(); + if (ADODB_ASSOC_CASE !== ADODB_ASSOC_CASE_NATIVE) { + $this->rsPrefix .= 'assoc_'; + } + $this->_bindInputArray = PHP_VERSION >= 5.1; + } + + + // the following should be compat with postgresql 7.2, + // which makes obsolete the LIMIT limit,offset syntax + function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false,$secs2cache=0) + { + $offsetStr = ($offset >= 0) ? " OFFSET ".((integer)$offset) : ''; + $limitStr = ($nrows >= 0) ? " LIMIT ".((integer)$nrows) : ''; + if ($secs2cache) + $rs = $this->CacheExecute($secs2cache,$sql."$limitStr$offsetStr",$inputarr); + else + $rs = $this->Execute($sql."$limitStr$offsetStr",$inputarr); + + return $rs; + } + + /* + function Prepare($sql) + { + $info = $this->ServerInfo(); + if ($info['version']>=7.3) { + return array($sql,false); + } + return $sql; + } + */ + + /** + * Generate the SQL to retrieve MetaColumns data + * @param string $table Table name + * @param string $schema Schema name (can be blank) + * @return string SQL statement to execute + */ + protected function _generateMetaColumnsSQL($table, $schema) + { + if ($schema) { + return sprintf($this->metaColumnsSQL1, $table, $table, $table, $schema); + } + else { + return sprintf($this->metaColumnsSQL, $table, $table, $schema); + } + } + + /** + * @returns assoc array where keys are tables, and values are foreign keys + */ + function MetaForeignKeys($table, $owner=false, $upper=false) + { + # Regex isolates the 2 terms between parenthesis using subexpressions + $regex = '^.*\((.*)\).*\((.*)\).*$'; + $sql=" + SELECT + lookup_table, + regexp_replace(consrc, '$regex', '\\2') AS lookup_field, + dep_table, + regexp_replace(consrc, '$regex', '\\1') AS dep_field + FROM ( + SELECT + pg_get_constraintdef(c.oid) AS consrc, + t.relname AS dep_table, + ft.relname AS lookup_table + FROM pg_constraint c + JOIN pg_class t ON (t.oid = c.conrelid) + JOIN pg_class ft ON (ft.oid = c.confrelid) + JOIN pg_namespace nft ON (nft.oid = ft.relnamespace) + LEFT JOIN pg_description ds ON (ds.objoid = c.oid) + JOIN pg_namespace n ON (n.oid = t.relnamespace) + WHERE c.contype = 'f'::\"char\" + ORDER BY t.relname, n.nspname, c.conname, c.oid + ) constraints + WHERE + dep_table='".strtolower($table)."' + ORDER BY + lookup_table, + dep_table, + dep_field"; + $rs = $this->Execute($sql); + + if (!$rs || $rs->EOF) return false; + + $a = array(); + while (!$rs->EOF) { + if ($upper) { + $a[strtoupper($rs->Fields('lookup_table'))][] = strtoupper(str_replace('"','',$rs->Fields('dep_field').'='.$rs->Fields('lookup_field'))); + } else { + $a[$rs->Fields('lookup_table')][] = str_replace('"','',$rs->Fields('dep_field').'='.$rs->Fields('lookup_field')); + } + $rs->MoveNext(); + } + + return $a; + + } + + // from Edward Jaramilla, improved version - works on pg 7.4 + function _old_MetaForeignKeys($table, $owner=false, $upper=false) + { + $sql = 'SELECT t.tgargs as args + FROM + pg_trigger t,pg_class c,pg_proc p + WHERE + t.tgenabled AND + t.tgrelid = c.oid AND + t.tgfoid = p.oid AND + p.proname = \'RI_FKey_check_ins\' AND + c.relname = \''.strtolower($table).'\' + ORDER BY + t.tgrelid'; + + $rs = $this->Execute($sql); + + if (!$rs || $rs->EOF) return false; + + $arr = $rs->GetArray(); + $a = array(); + foreach($arr as $v) { + $data = explode(chr(0), $v['args']); + $size = count($data)-1; //-1 because the last node is empty + for($i = 4; $i < $size; $i++) { + if ($upper) + $a[strtoupper($data[2])][] = strtoupper($data[$i].'='.$data[++$i]); + else + $a[$data[2]][] = $data[$i].'='.$data[++$i]; + } + } + return $a; + } + + function _query($sql,$inputarr=false) + { + if (! $this->_bindInputArray) { + // We don't have native support for parameterized queries, so let's emulate it at the parent + return ADODB_postgres64::_query($sql, $inputarr); + } + + $this->_pnum = 0; + $this->_errorMsg = false; + // -- added Cristiano da Cunha Duarte + if ($inputarr) { + $sqlarr = explode('?',trim($sql)); + $sql = ''; + $i = 1; + $last = sizeof($sqlarr)-1; + foreach($sqlarr as $v) { + if ($last < $i) $sql .= $v; + else $sql .= $v.' $'.$i; + $i++; + } + + $rez = pg_query_params($this->_connectionID,$sql, $inputarr); + } else { + $rez = pg_query($this->_connectionID,$sql); + } + // check if no data returned, then no need to create real recordset + if ($rez && pg_num_fields($rez) <= 0) { + if (is_resource($this->_resultid) && get_resource_type($this->_resultid) === 'pgsql result') { + pg_free_result($this->_resultid); + } + $this->_resultid = $rez; + return true; + } + return $rez; + } + + // this is a set of functions for managing client encoding - very important if the encodings + // of your database and your output target (i.e. HTML) don't match + //for instance, you may have UNICODE database and server it on-site as WIN1251 etc. + // GetCharSet - get the name of the character set the client is using now + // the functions should work with Postgres 7.0 and above, the set of charsets supported + // depends on compile flags of postgres distribution - if no charsets were compiled into the server + // it will return 'SQL_ANSI' always + function GetCharSet() + { + //we will use ADO's builtin property charSet + $this->charSet = @pg_client_encoding($this->_connectionID); + if (!$this->charSet) { + return false; + } else { + return $this->charSet; + } + } + + // SetCharSet - switch the client encoding + function SetCharSet($charset_name) + { + $this->GetCharSet(); + if ($this->charSet !== $charset_name) { + $if = pg_set_client_encoding($this->_connectionID, $charset_name); + if ($if == "0" & $this->GetCharSet() == $charset_name) { + return true; + } else return false; + } else return true; + } + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordSet_postgres7 extends ADORecordSet_postgres64{ + + var $databaseType = "postgres7"; + + + function __construct($queryID, $mode=false) + { + parent::__construct($queryID, $mode); + } + + // 10% speedup to move MoveNext to child class + function MoveNext() + { + if (!$this->EOF) { + $this->_currentRow++; + if ($this->_numOfRows < 0 || $this->_numOfRows > $this->_currentRow) { + $this->fields = @pg_fetch_array($this->_queryID,$this->_currentRow,$this->fetchMode); + + if (is_array($this->fields)) { + if ($this->fields && isset($this->_blobArr)) $this->_fixblobs(); + return true; + } + } + $this->fields = false; + $this->EOF = true; + } + return false; + } + +} + +class ADORecordSet_assoc_postgres7 extends ADORecordSet_postgres64{ + + var $databaseType = "postgres7"; + + + function __construct($queryID, $mode=false) + { + parent::__construct($queryID, $mode); + } + + function _fetch() + { + if ($this->_currentRow >= $this->_numOfRows && $this->_numOfRows >= 0) { + return false; + } + + $this->fields = @pg_fetch_array($this->_queryID,$this->_currentRow,$this->fetchMode); + + if ($this->fields) { + if (isset($this->_blobArr)) $this->_fixblobs(); + $this->_updatefields(); + } + + return (is_array($this->fields)); + } + + function MoveNext() + { + if (!$this->EOF) { + $this->_currentRow++; + if ($this->_numOfRows < 0 || $this->_numOfRows > $this->_currentRow) { + $this->fields = @pg_fetch_array($this->_queryID,$this->_currentRow,$this->fetchMode); + + if (is_array($this->fields)) { + if ($this->fields) { + if (isset($this->_blobArr)) $this->_fixblobs(); + + $this->_updatefields(); + } + return true; + } + } + + + $this->fields = false; + $this->EOF = true; + } + return false; + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-postgres8.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-postgres8.inc.php new file mode 100644 index 000000000..54b14abbc --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-postgres8.inc.php @@ -0,0 +1,50 @@ +GetOne("SELECT lastval()") + : $this->GetOne("SELECT currval(pg_get_serial_sequence('$table', '$column'))"); + } +} + +class ADORecordSet_postgres8 extends ADORecordSet_postgres7 +{ + var $databaseType = "postgres8"; +} + +class ADORecordSet_assoc_postgres8 extends ADORecordSet_assoc_postgres7 +{ + var $databaseType = "postgres8"; +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-postgres9.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-postgres9.inc.php new file mode 100644 index 000000000..fb589188e --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-postgres9.inc.php @@ -0,0 +1,32 @@ +curmode = SQL_CUR_USE_ODBC; + parent::__construct(); + } + + function ServerInfo() + { + $info = ADODB_odbc::ServerInfo(); + if (!$info['version'] && preg_match('/([0-9.]+)/',$info['description'],$matches)) { + $info['version'] = $matches[1]; + } + return $info; + } + + function MetaPrimaryKeys($table, $owner = false) + { + $table = $this->Quote(strtoupper($table)); + + return $this->GetCol("SELECT columnname FROM COLUMNS WHERE tablename=$table AND mode='KEY' ORDER BY pos"); + } + + function MetaIndexes ($table, $primary = FALSE, $owner = false) + { + $table = $this->Quote(strtoupper($table)); + + $sql = "SELECT INDEXNAME,TYPE,COLUMNNAME FROM INDEXCOLUMNS ". + " WHERE TABLENAME=$table". + " ORDER BY INDEXNAME,COLUMNNO"; + + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + + $rs = $this->Execute($sql); + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + if (!is_object($rs)) { + return FALSE; + } + + $indexes = array(); + while ($row = $rs->FetchRow()) { + $indexes[$row[0]]['unique'] = $row[1] == 'UNIQUE'; + $indexes[$row[0]]['columns'][] = $row[2]; + } + if ($primary) { + $indexes['SYSPRIMARYKEYINDEX'] = array( + 'unique' => True, // by definition + 'columns' => $this->GetCol("SELECT columnname FROM COLUMNS WHERE tablename=$table AND mode='KEY' ORDER BY pos"), + ); + } + return $indexes; + } + + function MetaColumns ($table, $normalize = true) + { + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + $table = $this->Quote(strtoupper($table)); + + $retarr = array(); + foreach($this->GetAll("SELECT COLUMNNAME,DATATYPE,LEN,DEC,NULLABLE,MODE,\"DEFAULT\",CASE WHEN \"DEFAULT\" IS NULL THEN 0 ELSE 1 END AS HAS_DEFAULT FROM COLUMNS WHERE tablename=$table ORDER BY pos") as $column) + { + $fld = new ADOFieldObject(); + $fld->name = $column[0]; + $fld->type = $column[1]; + $fld->max_length = $fld->type == 'LONG' ? 2147483647 : $column[2]; + $fld->scale = $column[3]; + $fld->not_null = $column[4] == 'NO'; + $fld->primary_key = $column[5] == 'KEY'; + if ($fld->has_default = $column[7]) { + if ($fld->primary_key && $column[6] == 'DEFAULT SERIAL (1)') { + $fld->auto_increment = true; + $fld->has_default = false; + } else { + $fld->default_value = $column[6]; + switch($fld->type) { + case 'VARCHAR': + case 'CHARACTER': + case 'LONG': + $fld->default_value = $column[6]; + break; + default: + $fld->default_value = trim($column[6]); + break; + } + } + } + $retarr[$fld->name] = $fld; + } + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + + return $retarr; + } + + function MetaColumnNames($table, $numIndexes = false, $useattnum = false) + { + $table = $this->Quote(strtoupper($table)); + + return $this->GetCol("SELECT columnname FROM COLUMNS WHERE tablename=$table ORDER BY pos"); + } + + // unlike it seems, this depends on the db-session and works in a multiuser environment + function _insertid($table,$column) + { + return empty($table) ? False : $this->GetOne("SELECT $table.CURRVAL FROM DUAL"); + } + + /* + SelectLimit implementation problems: + + The following will return random 10 rows as order by performed after "WHERE rowno<10" + which is not ideal... + + select * from table where rowno < 10 order by 1 + + This means that we have to use the adoconnection base class SelectLimit when + there is an "order by". + + See http://listserv.sap.com/pipermail/sapdb.general/2002-January/010405.html + */ + +}; + + +class ADORecordSet_sapdb extends ADORecordSet_odbc { + + var $databaseType = "sapdb"; + + function __construct($id,$mode=false) + { + parent::__construct($id,$mode); + } +} + +} //define diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-sqlanywhere.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-sqlanywhere.inc.php new file mode 100644 index 000000000..03a9da2a0 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-sqlanywhere.inc.php @@ -0,0 +1,165 @@ +create_blobvar($blobVarName); + + b) load blob var from file. $filename must be complete path + + $dbcon->load_blobvar_from_file($blobVarName, $filename); + + c) Use the $blobVarName in SQL insert or update statement in the values + clause: + + $recordSet = $dbconn->Execute('INSERT INTO tabname (idcol, blobcol) ' + . + 'VALUES (\'test\', ' . $blobVarName . ')'); + + instead of loading blob from a file, you can also load from + an unformatted (raw) blob variable: + $dbcon->load_blobvar_from_var($blobVarName, $varName); + + d) drop blob variable on db server to free up resources: + $dbconn->drop_blobvar($blobVarName); + + Sybase_SQLAnywhere data driver. Requires ODBC. + +*/ + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +if (!defined('_ADODB_ODBC_LAYER')) { + include(ADODB_DIR."/drivers/adodb-odbc.inc.php"); +} + +if (!defined('ADODB_SYBASE_SQLANYWHERE')){ + + define('ADODB_SYBASE_SQLANYWHERE',1); + + class ADODB_sqlanywhere extends ADODB_odbc { + var $databaseType = "sqlanywhere"; + var $hasInsertID = true; + + function _insertid() { + return $this->GetOne('select @@identity'); + } + + function create_blobvar($blobVarName) { + $this->Execute("create variable $blobVarName long binary"); + return; + } + + function drop_blobvar($blobVarName) { + $this->Execute("drop variable $blobVarName"); + return; + } + + function load_blobvar_from_file($blobVarName, $filename) { + $chunk_size = 1000; + + $fd = fopen ($filename, "rb"); + + $integer_chunks = (integer)filesize($filename) / $chunk_size; + $modulus = filesize($filename) % $chunk_size; + if ($modulus != 0){ + $integer_chunks += 1; + } + + for($loop=1;$loop<=$integer_chunks;$loop++){ + $contents = fread ($fd, $chunk_size); + $contents = bin2hex($contents); + + $hexstring = ''; + + for($loop2=0;$loop2qstr($hexstring); + + $this->Execute("set $blobVarName = $blobVarName || " . $hexstring); + } + + fclose ($fd); + return; + } + + function load_blobvar_from_var($blobVarName, &$varName) { + $chunk_size = 1000; + + $integer_chunks = (integer)strlen($varName) / $chunk_size; + $modulus = strlen($varName) % $chunk_size; + if ($modulus != 0){ + $integer_chunks += 1; + } + + for($loop=1;$loop<=$integer_chunks;$loop++){ + $contents = substr ($varName, (($loop - 1) * $chunk_size), $chunk_size); + $contents = bin2hex($contents); + + $hexstring = ''; + + for($loop2=0;$loop2qstr($hexstring); + + $this->Execute("set $blobVarName = $blobVarName || " . $hexstring); + } + + return; + } + + /* + Insert a null into the blob field of the table first. + Then use UpdateBlob to store the blob. + + Usage: + + $conn->Execute('INSERT INTO blobtable (id, blobcol) VALUES (1, null)'); + $conn->UpdateBlob('blobtable','blobcol',$blob,'id=1'); + */ + function UpdateBlob($table,$column,&$val,$where,$blobtype='BLOB') + { + $blobVarName = 'hold_blob'; + $this->create_blobvar($blobVarName); + $this->load_blobvar_from_var($blobVarName, $val); + $this->Execute("UPDATE $table SET $column=$blobVarName WHERE $where"); + $this->drop_blobvar($blobVarName); + return true; + } + }; //class + + class ADORecordSet_sqlanywhere extends ADORecordSet_odbc { + + var $databaseType = "sqlanywhere"; + + function __construct($id,$mode=false) + { + parent::__construct($id,$mode); + } + + + }; //class + + +} //define diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-sqlite.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-sqlite.inc.php new file mode 100644 index 000000000..5cc6b9ea3 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-sqlite.inc.php @@ -0,0 +1,451 @@ +transOff) { + return true; + } + $ret = $this->Execute("BEGIN TRANSACTION"); + $this->transCnt += 1; + return true; + } + + function CommitTrans($ok=true) + { + if ($this->transOff) { + return true; + } + if (!$ok) { + return $this->RollbackTrans(); + } + $ret = $this->Execute("COMMIT"); + if ($this->transCnt > 0) { + $this->transCnt -= 1; + } + return !empty($ret); + } + + function RollbackTrans() + { + if ($this->transOff) { + return true; + } + $ret = $this->Execute("ROLLBACK"); + if ($this->transCnt > 0) { + $this->transCnt -= 1; + } + return !empty($ret); + } + + // mark newnham + function MetaColumns($table, $normalize=true) + { + global $ADODB_FETCH_MODE; + $false = false; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + if ($this->fetchMode !== false) { + $savem = $this->SetFetchMode(false); + } + $rs = $this->Execute("PRAGMA table_info('$table')"); + if (isset($savem)) { + $this->SetFetchMode($savem); + } + if (!$rs) { + $ADODB_FETCH_MODE = $save; + return $false; + } + $arr = array(); + while ($r = $rs->FetchRow()) { + $type = explode('(',$r['type']); + $size = ''; + if (sizeof($type)==2) { + $size = trim($type[1],')'); + } + $fn = strtoupper($r['name']); + $fld = new ADOFieldObject; + $fld->name = $r['name']; + $fld->type = $type[0]; + $fld->max_length = $size; + $fld->not_null = $r['notnull']; + $fld->default_value = $r['dflt_value']; + $fld->scale = 0; + if (isset($r['pk']) && $r['pk']) { + $fld->primary_key=1; + } + if ($save == ADODB_FETCH_NUM) { + $arr[] = $fld; + } else { + $arr[strtoupper($fld->name)] = $fld; + } + } + $rs->Close(); + $ADODB_FETCH_MODE = $save; + return $arr; + } + + function _init($parentDriver) + { + $parentDriver->hasTransactions = false; + $parentDriver->hasInsertID = true; + } + + function _insertid() + { + return sqlite_last_insert_rowid($this->_connectionID); + } + + function _affectedrows() + { + return sqlite_changes($this->_connectionID); + } + + function ErrorMsg() + { + if ($this->_logsql) { + return $this->_errorMsg; + } + return ($this->_errorNo) ? sqlite_error_string($this->_errorNo) : ''; + } + + function ErrorNo() + { + return $this->_errorNo; + } + + function SQLDate($fmt, $col=false) + { + $fmt = $this->qstr($fmt); + return ($col) ? "adodb_date2($fmt,$col)" : "adodb_date($fmt)"; + } + + + function _createFunctions() + { + @sqlite_create_function($this->_connectionID, 'adodb_date', 'adodb_date', 1); + @sqlite_create_function($this->_connectionID, 'adodb_date2', 'adodb_date2', 2); + } + + + // returns true or false + function _connect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + if (!function_exists('sqlite_open')) { + return null; + } + if (empty($argHostname) && $argDatabasename) { + $argHostname = $argDatabasename; + } + + $this->_connectionID = sqlite_open($argHostname); + if ($this->_connectionID === false) { + return false; + } + $this->_createFunctions(); + return true; + } + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + if (!function_exists('sqlite_open')) { + return null; + } + if (empty($argHostname) && $argDatabasename) { + $argHostname = $argDatabasename; + } + + $this->_connectionID = sqlite_popen($argHostname); + if ($this->_connectionID === false) { + return false; + } + $this->_createFunctions(); + return true; + } + + // returns query ID if successful, otherwise false + function _query($sql,$inputarr=false) + { + $rez = sqlite_query($sql,$this->_connectionID); + if (!$rez) { + $this->_errorNo = sqlite_last_error($this->_connectionID); + } + // If no data was returned, we don't need to create a real recordset + // Note: this code is untested, as I don't have a sqlite2 setup available + elseif (sqlite_num_fields($rez) == 0) { + $rez = true; + } + + return $rez; + } + + function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false,$secs2cache=0) + { + $offsetStr = ($offset >= 0) ? " OFFSET $offset" : ''; + $limitStr = ($nrows >= 0) ? " LIMIT $nrows" : ($offset >= 0 ? ' LIMIT 999999999' : ''); + if ($secs2cache) { + $rs = $this->CacheExecute($secs2cache,$sql."$limitStr$offsetStr",$inputarr); + } else { + $rs = $this->Execute($sql."$limitStr$offsetStr",$inputarr); + } + + return $rs; + } + + /* + This algorithm is not very efficient, but works even if table locking + is not available. + + Will return false if unable to generate an ID after $MAXLOOPS attempts. + */ + var $_genSeqSQL = "create table %s (id integer)"; + + function GenID($seq='adodbseq',$start=1) + { + // if you have to modify the parameter below, your database is overloaded, + // or you need to implement generation of id's yourself! + $MAXLOOPS = 100; + //$this->debug=1; + while (--$MAXLOOPS>=0) { + @($num = $this->GetOne("select id from $seq")); + if ($num === false) { + $this->Execute(sprintf($this->_genSeqSQL ,$seq)); + $start -= 1; + $num = '0'; + $ok = $this->Execute("insert into $seq values($start)"); + if (!$ok) { + return false; + } + } + $this->Execute("update $seq set id=id+1 where id=$num"); + + if ($this->affected_rows() > 0) { + $num += 1; + $this->genID = $num; + return $num; + } + } + if ($fn = $this->raiseErrorFn) { + $fn($this->databaseType,'GENID',-32000,"Unable to generate unique id after $MAXLOOPS attempts",$seq,$num); + } + return false; + } + + function CreateSequence($seqname='adodbseq',$start=1) + { + if (empty($this->_genSeqSQL)) { + return false; + } + $ok = $this->Execute(sprintf($this->_genSeqSQL,$seqname)); + if (!$ok) { + return false; + } + $start -= 1; + return $this->Execute("insert into $seqname values($start)"); + } + + var $_dropSeqSQL = 'drop table %s'; + function DropSequence($seqname = 'adodbseq') + { + if (empty($this->_dropSeqSQL)) { + return false; + } + return $this->Execute(sprintf($this->_dropSeqSQL,$seqname)); + } + + // returns true or false + function _close() + { + return @sqlite_close($this->_connectionID); + } + + function MetaIndexes($table, $primary = FALSE, $owner = false) + { + $false = false; + // save old fetch mode + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + $SQL=sprintf("SELECT name,sql FROM sqlite_master WHERE type='index' AND tbl_name='%s'", strtolower($table)); + $rs = $this->Execute($SQL); + if (!is_object($rs)) { + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + return $false; + } + + $indexes = array (); + while ($row = $rs->FetchRow()) { + if ($primary && preg_match("/primary/i",$row[1]) == 0) { + continue; + } + if (!isset($indexes[$row[0]])) { + $indexes[$row[0]] = array( + 'unique' => preg_match("/unique/i",$row[1]), + 'columns' => array() + ); + } + /** + * There must be a more elegant way of doing this, + * the index elements appear in the SQL statement + * in cols[1] between parentheses + * e.g CREATE UNIQUE INDEX ware_0 ON warehouse (org,warehouse) + */ + $cols = explode("(",$row[1]); + $cols = explode(")",$cols[1]); + array_pop($cols); + $indexes[$row[0]]['columns'] = $cols; + } + if (isset($savem)) { + $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + } + return $indexes; + } + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordset_sqlite extends ADORecordSet { + + var $databaseType = "sqlite"; + var $bind = false; + + function __construct($queryID,$mode=false) + { + + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + switch($mode) { + case ADODB_FETCH_NUM: + $this->fetchMode = SQLITE_NUM; + break; + case ADODB_FETCH_ASSOC: + $this->fetchMode = SQLITE_ASSOC; + break; + default: + $this->fetchMode = SQLITE_BOTH; + break; + } + $this->adodbFetchMode = $mode; + + $this->_queryID = $queryID; + + $this->_inited = true; + $this->fields = array(); + if ($queryID) { + $this->_currentRow = 0; + $this->EOF = !$this->_fetch(); + @$this->_initrs(); + } else { + $this->_numOfRows = 0; + $this->_numOfFields = 0; + $this->EOF = true; + } + + return $this->_queryID; + } + + + function FetchField($fieldOffset = -1) + { + $fld = new ADOFieldObject; + $fld->name = sqlite_field_name($this->_queryID, $fieldOffset); + $fld->type = 'VARCHAR'; + $fld->max_length = -1; + return $fld; + } + + function _initrs() + { + $this->_numOfRows = @sqlite_num_rows($this->_queryID); + $this->_numOfFields = @sqlite_num_fields($this->_queryID); + } + + function Fields($colname) + { + if ($this->fetchMode != SQLITE_NUM) { + return $this->fields[$colname]; + } + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + function _seek($row) + { + return sqlite_seek($this->_queryID, $row); + } + + function _fetch($ignore_fields=false) + { + $this->fields = @sqlite_fetch_array($this->_queryID,$this->fetchMode); + return !empty($this->fields); + } + + function _close() + { + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-sqlite3.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-sqlite3.inc.php new file mode 100644 index 000000000..7600c3a96 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-sqlite3.inc.php @@ -0,0 +1,438 @@ +transOff) { + return true; + } + $ret = $this->Execute("BEGIN TRANSACTION"); + $this->transCnt += 1; + return true; + } + + function CommitTrans($ok=true) + { + if ($this->transOff) { + return true; + } + if (!$ok) { + return $this->RollbackTrans(); + } + $ret = $this->Execute("COMMIT"); + if ($this->transCnt > 0) { + $this->transCnt -= 1; + } + return !empty($ret); + } + + function RollbackTrans() + { + if ($this->transOff) { + return true; + } + $ret = $this->Execute("ROLLBACK"); + if ($this->transCnt > 0) { + $this->transCnt -= 1; + } + return !empty($ret); + } + + // mark newnham + function MetaColumns($table, $normalize=true) + { + global $ADODB_FETCH_MODE; + $false = false; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + if ($this->fetchMode !== false) { + $savem = $this->SetFetchMode(false); + } + $rs = $this->Execute("PRAGMA table_info('$table')"); + if (isset($savem)) { + $this->SetFetchMode($savem); + } + if (!$rs) { + $ADODB_FETCH_MODE = $save; + return $false; + } + $arr = array(); + while ($r = $rs->FetchRow()) { + $type = explode('(',$r['type']); + $size = ''; + if (sizeof($type)==2) { + $size = trim($type[1],')'); + } + $fn = strtoupper($r['name']); + $fld = new ADOFieldObject; + $fld->name = $r['name']; + $fld->type = $type[0]; + $fld->max_length = $size; + $fld->not_null = $r['notnull']; + $fld->default_value = $r['dflt_value']; + $fld->scale = 0; + if (isset($r['pk']) && $r['pk']) { + $fld->primary_key=1; + } + if ($save == ADODB_FETCH_NUM) { + $arr[] = $fld; + } else { + $arr[strtoupper($fld->name)] = $fld; + } + } + $rs->Close(); + $ADODB_FETCH_MODE = $save; + return $arr; + } + + function _init($parentDriver) + { + $parentDriver->hasTransactions = false; + $parentDriver->hasInsertID = true; + } + + function _insertid() + { + return $this->_connectionID->lastInsertRowID(); + } + + function _affectedrows() + { + return $this->_connectionID->changes(); + } + + function ErrorMsg() + { + if ($this->_logsql) { + return $this->_errorMsg; + } + return ($this->_errorNo) ? $this->ErrorNo() : ''; //**tochange? + } + + function ErrorNo() + { + return $this->_connectionID->lastErrorCode(); //**tochange?? + } + + function SQLDate($fmt, $col=false) + { + $fmt = $this->qstr($fmt); + return ($col) ? "adodb_date2($fmt,$col)" : "adodb_date($fmt)"; + } + + + function _createFunctions() + { + $this->_connectionID->createFunction('adodb_date', 'adodb_date', 1); + $this->_connectionID->createFunction('adodb_date2', 'adodb_date2', 2); + } + + + // returns true or false + function _connect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + if (empty($argHostname) && $argDatabasename) { + $argHostname = $argDatabasename; + } + $this->_connectionID = new SQLite3($argHostname); + $this->_createFunctions(); + + return true; + } + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + // There's no permanent connect in SQLite3 + return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabasename); + } + + // returns query ID if successful, otherwise false + function _query($sql,$inputarr=false) + { + $rez = $this->_connectionID->query($sql); + if ($rez === false) { + $this->_errorNo = $this->_connectionID->lastErrorCode(); + } + // If no data was returned, we don't need to create a real recordset + elseif ($rez->numColumns() == 0) { + $rez->finalize(); + $rez = true; + } + + return $rez; + } + + function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false,$secs2cache=0) + { + $offsetStr = ($offset >= 0) ? " OFFSET $offset" : ''; + $limitStr = ($nrows >= 0) ? " LIMIT $nrows" : ($offset >= 0 ? ' LIMIT 999999999' : ''); + if ($secs2cache) { + $rs = $this->CacheExecute($secs2cache,$sql."$limitStr$offsetStr",$inputarr); + } else { + $rs = $this->Execute($sql."$limitStr$offsetStr",$inputarr); + } + + return $rs; + } + + /* + This algorithm is not very efficient, but works even if table locking + is not available. + + Will return false if unable to generate an ID after $MAXLOOPS attempts. + */ + var $_genSeqSQL = "create table %s (id integer)"; + + function GenID($seq='adodbseq',$start=1) + { + // if you have to modify the parameter below, your database is overloaded, + // or you need to implement generation of id's yourself! + $MAXLOOPS = 100; + //$this->debug=1; + while (--$MAXLOOPS>=0) { + @($num = $this->GetOne("select id from $seq")); + if ($num === false) { + $this->Execute(sprintf($this->_genSeqSQL ,$seq)); + $start -= 1; + $num = '0'; + $ok = $this->Execute("insert into $seq values($start)"); + if (!$ok) { + return false; + } + } + $this->Execute("update $seq set id=id+1 where id=$num"); + + if ($this->affected_rows() > 0) { + $num += 1; + $this->genID = $num; + return $num; + } + } + if ($fn = $this->raiseErrorFn) { + $fn($this->databaseType,'GENID',-32000,"Unable to generate unique id after $MAXLOOPS attempts",$seq,$num); + } + return false; + } + + function CreateSequence($seqname='adodbseq',$start=1) + { + if (empty($this->_genSeqSQL)) { + return false; + } + $ok = $this->Execute(sprintf($this->_genSeqSQL,$seqname)); + if (!$ok) { + return false; + } + $start -= 1; + return $this->Execute("insert into $seqname values($start)"); + } + + var $_dropSeqSQL = 'drop table %s'; + function DropSequence($seqname = 'adodbseq') + { + if (empty($this->_dropSeqSQL)) { + return false; + } + return $this->Execute(sprintf($this->_dropSeqSQL,$seqname)); + } + + // returns true or false + function _close() + { + return $this->_connectionID->close(); + } + + function MetaIndexes($table, $primary = FALSE, $owner = false) + { + $false = false; + // save old fetch mode + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->fetchMode !== FALSE) { + $savem = $this->SetFetchMode(FALSE); + } + $SQL=sprintf("SELECT name,sql FROM sqlite_master WHERE type='index' AND tbl_name='%s'", strtolower($table)); + $rs = $this->Execute($SQL); + if (!is_object($rs)) { + if (isset($savem)) { + $this->SetFetchMode($savem); + } + $ADODB_FETCH_MODE = $save; + return $false; + } + + $indexes = array (); + while ($row = $rs->FetchRow()) { + if ($primary && preg_match("/primary/i",$row[1]) == 0) { + continue; + } + if (!isset($indexes[$row[0]])) { + $indexes[$row[0]] = array( + 'unique' => preg_match("/unique/i",$row[1]), + 'columns' => array() + ); + } + /** + * There must be a more elegant way of doing this, + * the index elements appear in the SQL statement + * in cols[1] between parentheses + * e.g CREATE UNIQUE INDEX ware_0 ON warehouse (org,warehouse) + */ + $cols = explode("(",$row[1]); + $cols = explode(")",$cols[1]); + array_pop($cols); + $indexes[$row[0]]['columns'] = $cols; + } + if (isset($savem)) { + $this->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + } + return $indexes; + } + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + +class ADORecordset_sqlite3 extends ADORecordSet { + + var $databaseType = "sqlite3"; + var $bind = false; + + function __construct($queryID,$mode=false) + { + + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + switch($mode) { + case ADODB_FETCH_NUM: + $this->fetchMode = SQLITE3_NUM; + break; + case ADODB_FETCH_ASSOC: + $this->fetchMode = SQLITE3_ASSOC; + break; + default: + $this->fetchMode = SQLITE3_BOTH; + break; + } + $this->adodbFetchMode = $mode; + + $this->_queryID = $queryID; + + $this->_inited = true; + $this->fields = array(); + if ($queryID) { + $this->_currentRow = 0; + $this->EOF = !$this->_fetch(); + @$this->_initrs(); + } else { + $this->_numOfRows = 0; + $this->_numOfFields = 0; + $this->EOF = true; + } + + return $this->_queryID; + } + + + function FetchField($fieldOffset = -1) + { + $fld = new ADOFieldObject; + $fld->name = $this->_queryID->columnName($fieldOffset); + $fld->type = 'VARCHAR'; + $fld->max_length = -1; + return $fld; + } + + function _initrs() + { + $this->_numOfFields = $this->_queryID->numColumns(); + + } + + function Fields($colname) + { + if ($this->fetchMode != SQLITE3_NUM) { + return $this->fields[$colname]; + } + if (!$this->bind) { + $this->bind = array(); + for ($i=0; $i < $this->_numOfFields; $i++) { + $o = $this->FetchField($i); + $this->bind[strtoupper($o->name)] = $i; + } + } + + return $this->fields[$this->bind[strtoupper($colname)]]; + } + + function _seek($row) + { + // sqlite3 does not implement seek + if ($this->debug) { + ADOConnection::outp("SQLite3 does not implement seek"); + } + return false; + } + + function _fetch($ignore_fields=false) + { + $this->fields = $this->_queryID->fetchArray($this->fetchMode); + return !empty($this->fields); + } + + function _close() + { + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-sqlitepo.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-sqlitepo.inc.php new file mode 100644 index 000000000..b167e8c8b --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-sqlitepo.inc.php @@ -0,0 +1,58 @@ +fields = array(); + $fields = @sqlite_fetch_array($this->_queryID,$this->fetchMode); + if(is_array($fields)) + foreach($fields as $n => $v) + { + if(($p = strpos($n, ".")) !== false) + $n = substr($n, $p+1); + $this->fields[$n] = $v; + } + + return !empty($this->fields); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-sybase.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-sybase.inc.php new file mode 100644 index 000000000..6ba392d35 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-sybase.inc.php @@ -0,0 +1,443 @@ +GetOne('select @@identity'); + } + // might require begintrans -- committrans + function _affectedrows() + { + return $this->GetOne('select @@rowcount'); + } + + + function BeginTrans() + { + + if ($this->transOff) return true; + $this->transCnt += 1; + + $this->Execute('BEGIN TRAN'); + return true; + } + + function CommitTrans($ok=true) + { + if ($this->transOff) return true; + + if (!$ok) return $this->RollbackTrans(); + + $this->transCnt -= 1; + $this->Execute('COMMIT TRAN'); + return true; + } + + function RollbackTrans() + { + if ($this->transOff) return true; + $this->transCnt -= 1; + $this->Execute('ROLLBACK TRAN'); + return true; + } + + // http://www.isug.com/Sybase_FAQ/ASE/section6.1.html#6.1.4 + function RowLock($tables,$where,$col='top 1 null as ignore') + { + if (!$this->_hastrans) $this->BeginTrans(); + $tables = str_replace(',',' HOLDLOCK,',$tables); + return $this->GetOne("select $col from $tables HOLDLOCK where $where"); + + } + + function SelectDB($dbName) + { + $this->database = $dbName; + $this->databaseName = $dbName; # obsolete, retained for compat with older adodb versions + if ($this->_connectionID) { + return @sybase_select_db($dbName); + } + else return false; + } + + /* Returns: the last error message from previous database operation + Note: This function is NOT available for Microsoft SQL Server. */ + + + function ErrorMsg() + { + if ($this->_logsql) return $this->_errorMsg; + if (function_exists('sybase_get_last_message')) + $this->_errorMsg = sybase_get_last_message(); + else + $this->_errorMsg = isset($php_errormsg) ? $php_errormsg : 'SYBASE error messages not supported on this platform'; + return $this->_errorMsg; + } + + // returns true or false + function _connect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + if (!function_exists('sybase_connect')) return null; + + // Sybase connection on custom port + if ($this->port) { + $argHostname .= ':' . $this->port; + } + + if ($this->charSet) { + $this->_connectionID = sybase_connect($argHostname,$argUsername,$argPassword, $this->charSet); + } else { + $this->_connectionID = sybase_connect($argHostname,$argUsername,$argPassword); + } + + if ($this->_connectionID === false) return false; + if ($argDatabasename) return $this->SelectDB($argDatabasename); + return true; + } + + // returns true or false + function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename) + { + if (!function_exists('sybase_connect')) return null; + + // Sybase connection on custom port + if ($this->port) { + $argHostname .= ':' . $this->port; + } + + if ($this->charSet) { + $this->_connectionID = sybase_pconnect($argHostname,$argUsername,$argPassword, $this->charSet); + } else { + $this->_connectionID = sybase_pconnect($argHostname,$argUsername,$argPassword); + } + + if ($this->_connectionID === false) return false; + if ($argDatabasename) return $this->SelectDB($argDatabasename); + return true; + } + + // returns query ID if successful, otherwise false + function _query($sql,$inputarr=false) + { + global $ADODB_COUNTRECS; + + if ($ADODB_COUNTRECS == false && ADODB_PHPVER >= 0x4300) + return sybase_unbuffered_query($sql,$this->_connectionID); + else + return sybase_query($sql,$this->_connectionID); + } + + // See http://www.isug.com/Sybase_FAQ/ASE/section6.2.html#6.2.12 + function SelectLimit($sql,$nrows=-1,$offset=-1,$inputarr=false,$secs2cache=0) + { + if ($secs2cache > 0) {// we do not cache rowcount, so we have to load entire recordset + $rs = ADOConnection::SelectLimit($sql,$nrows,$offset,$inputarr,$secs2cache); + return $rs; + } + + $nrows = (integer) $nrows; + $offset = (integer) $offset; + + $cnt = ($nrows >= 0) ? $nrows : 999999999; + if ($offset > 0 && $cnt) $cnt += $offset; + + $this->Execute("set rowcount $cnt"); + $rs = ADOConnection::SelectLimit($sql,$nrows,$offset,$inputarr,0); + $this->Execute("set rowcount 0"); + + return $rs; + } + + // returns true or false + function _close() + { + return @sybase_close($this->_connectionID); + } + + static function UnixDate($v) + { + return ADORecordSet_array_sybase::UnixDate($v); + } + + static function UnixTimeStamp($v) + { + return ADORecordSet_array_sybase::UnixTimeStamp($v); + } + + + + # Added 2003-10-05 by Chris Phillipson + # Used ASA SQL Reference Manual -- http://sybooks.sybase.com/onlinebooks/group-aw/awg0800e/dbrfen8/@ebt-link;pt=16756?target=%25N%15_12018_START_RESTART_N%25 + # to convert similar Microsoft SQL*Server (mssql) API into Sybase compatible version + // Format date column in sql string given an input format that understands Y M D + function SQLDate($fmt, $col=false) + { + if (!$col) $col = $this->sysTimeStamp; + $s = ''; + + $len = strlen($fmt); + for ($i=0; $i < $len; $i++) { + if ($s) $s .= '+'; + $ch = $fmt[$i]; + switch($ch) { + case 'Y': + case 'y': + $s .= "datename(yy,$col)"; + break; + case 'M': + $s .= "convert(char(3),$col,0)"; + break; + case 'm': + $s .= "str_replace(str(month($col),2),' ','0')"; + break; + case 'Q': + case 'q': + $s .= "datename(qq,$col)"; + break; + case 'D': + case 'd': + $s .= "str_replace(str(datepart(dd,$col),2),' ','0')"; + break; + case 'h': + $s .= "substring(convert(char(14),$col,0),13,2)"; + break; + + case 'H': + $s .= "str_replace(str(datepart(hh,$col),2),' ','0')"; + break; + + case 'i': + $s .= "str_replace(str(datepart(mi,$col),2),' ','0')"; + break; + case 's': + $s .= "str_replace(str(datepart(ss,$col),2),' ','0')"; + break; + case 'a': + case 'A': + $s .= "substring(convert(char(19),$col,0),18,2)"; + break; + + default: + if ($ch == '\\') { + $i++; + $ch = substr($fmt,$i,1); + } + $s .= $this->qstr($ch); + break; + } + } + return $s; + } + + # Added 2003-10-07 by Chris Phillipson + # Used ASA SQL Reference Manual -- http://sybooks.sybase.com/onlinebooks/group-aw/awg0800e/dbrfen8/@ebt-link;pt=5981;uf=0?target=0;window=new;showtoc=true;book=dbrfen8 + # to convert similar Microsoft SQL*Server (mssql) API into Sybase compatible version + function MetaPrimaryKeys($table, $owner = false) + { + $sql = "SELECT c.column_name " . + "FROM syscolumn c, systable t " . + "WHERE t.table_name='$table' AND c.table_id=t.table_id " . + "AND t.table_type='BASE' " . + "AND c.pkey = 'Y' " . + "ORDER BY c.column_id"; + + $a = $this->GetCol($sql); + if ($a && sizeof($a)>0) return $a; + return false; + } +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ +global $ADODB_sybase_mths; +$ADODB_sybase_mths = array( + 'JAN'=>1,'FEB'=>2,'MAR'=>3,'APR'=>4,'MAY'=>5,'JUN'=>6, + 'JUL'=>7,'AUG'=>8,'SEP'=>9,'OCT'=>10,'NOV'=>11,'DEC'=>12); + +class ADORecordset_sybase extends ADORecordSet { + + var $databaseType = "sybase"; + var $canSeek = true; + // _mths works only in non-localised system + var $_mths = array('JAN'=>1,'FEB'=>2,'MAR'=>3,'APR'=>4,'MAY'=>5,'JUN'=>6,'JUL'=>7,'AUG'=>8,'SEP'=>9,'OCT'=>10,'NOV'=>11,'DEC'=>12); + + function __construct($id,$mode=false) + { + if ($mode === false) { + global $ADODB_FETCH_MODE; + $mode = $ADODB_FETCH_MODE; + } + if (!$mode) $this->fetchMode = ADODB_FETCH_ASSOC; + else $this->fetchMode = $mode; + parent::__construct($id,$mode); + } + + /* Returns: an object containing field information. + Get column information in the Recordset object. fetchField() can be used in order to obtain information about + fields in a certain query result. If the field offset isn't specified, the next field that wasn't yet retrieved by + fetchField() is retrieved. */ + function FetchField($fieldOffset = -1) + { + if ($fieldOffset != -1) { + $o = @sybase_fetch_field($this->_queryID, $fieldOffset); + } + else if ($fieldOffset == -1) { /* The $fieldOffset argument is not provided thus its -1 */ + $o = @sybase_fetch_field($this->_queryID); + } + // older versions of PHP did not support type, only numeric + if ($o && !isset($o->type)) $o->type = ($o->numeric) ? 'float' : 'varchar'; + return $o; + } + + function _initrs() + { + global $ADODB_COUNTRECS; + $this->_numOfRows = ($ADODB_COUNTRECS)? @sybase_num_rows($this->_queryID):-1; + $this->_numOfFields = @sybase_num_fields($this->_queryID); + } + + function _seek($row) + { + return @sybase_data_seek($this->_queryID, $row); + } + + function _fetch($ignore_fields=false) + { + if ($this->fetchMode == ADODB_FETCH_NUM) { + $this->fields = @sybase_fetch_row($this->_queryID); + } else if ($this->fetchMode == ADODB_FETCH_ASSOC) { + $this->fields = @sybase_fetch_assoc($this->_queryID); + + if (is_array($this->fields)) { + $this->fields = $this->GetRowAssoc(); + return true; + } + return false; + } else { + $this->fields = @sybase_fetch_array($this->_queryID); + } + if ( is_array($this->fields)) { + return true; + } + + return false; + } + + /* close() only needs to be called if you are worried about using too much memory while your script + is running. All associated result memory for the specified result identifier will automatically be freed. */ + function _close() { + return @sybase_free_result($this->_queryID); + } + + // sybase/mssql uses a default date like Dec 30 2000 12:00AM + static function UnixDate($v) + { + return ADORecordSet_array_sybase::UnixDate($v); + } + + static function UnixTimeStamp($v) + { + return ADORecordSet_array_sybase::UnixTimeStamp($v); + } +} + +class ADORecordSet_array_sybase extends ADORecordSet_array { + function __construct($id=-1) + { + parent::__construct($id); + } + + // sybase/mssql uses a default date like Dec 30 2000 12:00AM + static function UnixDate($v) + { + global $ADODB_sybase_mths; + + //Dec 30 2000 12:00AM + if (!preg_match( "/([A-Za-z]{3})[-/\. ]+([0-9]{1,2})[-/\. ]+([0-9]{4})/" + ,$v, $rr)) return parent::UnixDate($v); + + if ($rr[3] <= TIMESTAMP_FIRST_YEAR) return 0; + + $themth = substr(strtoupper($rr[1]),0,3); + $themth = $ADODB_sybase_mths[$themth]; + if ($themth <= 0) return false; + // h-m-s-MM-DD-YY + return adodb_mktime(0,0,0,$themth,$rr[2],$rr[3]); + } + + static function UnixTimeStamp($v) + { + global $ADODB_sybase_mths; + //11.02.2001 Toni Tunkkari toni.tunkkari@finebyte.com + //Changed [0-9] to [0-9 ] in day conversion + if (!preg_match( "/([A-Za-z]{3})[-/\. ]([0-9 ]{1,2})[-/\. ]([0-9]{4}) +([0-9]{1,2}):([0-9]{1,2}) *([apAP]{0,1})/" + ,$v, $rr)) return parent::UnixTimeStamp($v); + if ($rr[3] <= TIMESTAMP_FIRST_YEAR) return 0; + + $themth = substr(strtoupper($rr[1]),0,3); + $themth = $ADODB_sybase_mths[$themth]; + if ($themth <= 0) return false; + + switch (strtoupper($rr[6])) { + case 'P': + if ($rr[4]<12) $rr[4] += 12; + break; + case 'A': + if ($rr[4]==12) $rr[4] = 0; + break; + default: + break; + } + // h-m-s-MM-DD-YY + return adodb_mktime($rr[4],$rr[5],0,$themth,$rr[2],$rr[3]); + } +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-sybase_ase.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-sybase_ase.inc.php new file mode 100644 index 000000000..669dd22ff --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-sybase_ase.inc.php @@ -0,0 +1,120 @@ +metaTablesSQL) { + // complicated state saving by the need for backward compat + + if ($ttype == 'VIEWS'){ + $sql = str_replace('U', 'V', $this->metaTablesSQL); + }elseif (false === $ttype){ + $sql = str_replace('U',"U' OR type='V", $this->metaTablesSQL); + }else{ // TABLES OR ANY OTHER + $sql = $this->metaTablesSQL; + } + $rs = $this->Execute($sql); + + if ($rs === false || !method_exists($rs, 'GetArray')){ + return $false; + } + $arr = $rs->GetArray(); + + $arr2 = array(); + foreach($arr as $key=>$value){ + $arr2[] = trim($value['name']); + } + return $arr2; + } + return $false; + } + + function MetaDatabases() + { + $arr = array(); + if ($this->metaDatabasesSQL!='') { + $rs = $this->Execute($this->metaDatabasesSQL); + if ($rs && !$rs->EOF){ + while (!$rs->EOF){ + $arr[] = $rs->Fields('name'); + $rs->MoveNext(); + } + return $arr; + } + } + return false; + } + + // fix a bug which prevent the metaColumns query to be executed for Sybase ASE + function MetaColumns($table,$upper=false) + { + $false = false; + if (!empty($this->metaColumnsSQL)) { + + $rs = $this->Execute(sprintf($this->metaColumnsSQL,$table)); + if ($rs === false) return $false; + + $retarr = array(); + while (!$rs->EOF) { + $fld = new ADOFieldObject(); + $fld->name = $rs->Fields('field_name'); + $fld->type = $rs->Fields('type'); + $fld->max_length = $rs->Fields('width'); + $retarr[strtoupper($fld->name)] = $fld; + $rs->MoveNext(); + } + $rs->Close(); + return $retarr; + } + return $false; + } + + function getProcedureList($schema) + { + return false; + } + + function ErrorMsg() + { + if (!function_exists('sybase_connect')){ + return 'Your PHP doesn\'t contain the Sybase connection module!'; + } + return parent::ErrorMsg(); + } +} + +class adorecordset_sybase_ase extends ADORecordset_sybase { +var $databaseType = "sybase_ase"; +function __construct($id,$mode=false) + { + parent::__construct($id,$mode); + } + +} diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-text.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-text.inc.php new file mode 100644 index 000000000..347167a75 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-text.inc.php @@ -0,0 +1,389 @@ +Connect($array,[$types],[$colnames]); + + Parameter $array is the 2 dimensional array of data. The first row can contain the + column names. If column names is not defined in first row, you MUST define $colnames, + the 3rd parameter. + + Parameter $types is optional. If defined, it should contain an array matching + the number of columns in $array, with each element matching the correct type defined + by MetaType: (B,C,I,L,N). If undefined, we will probe for $this->_proberows rows + to guess the type. Only C,I and N are recognised. + + Parameter $colnames is optional. If defined, it is an array that contains the + column names of $array. If undefined, we assume the first row of $array holds the + column names. + + The Execute() function will return a recordset. The recordset works like a normal recordset. + We have partial support for SQL parsing. We process the SQL using the following rules: + + 1. SQL order by's always work for the first column ordered. Subsequent cols are ignored + + 2. All operations take place on the same table. No joins possible. In fact the FROM clause + is ignored! You can use any name for the table. + + 3. To simplify code, all columns are returned, except when selecting 1 column + + $rs = $db->Execute('select col1,col2 from table'); // sql ignored, will generate all cols + + We special case handling of 1 column because it is used in filter popups + + $rs = $db->Execute('select col1 from table'); + // sql accepted and processed -- any table name is accepted + + $rs = $db->Execute('select distinct col1 from table'); + // sql accepted and processed + +4. Where clauses are ignored, but searching with the 3rd parameter of Execute is permitted. + This has to use PHP syntax and we will eval() it. You can even use PHP functions. + + $rs = $db->Execute('select * from table',false,"\$COL1='abc' and $\COL2=3") + // the 3rd param is searched -- make sure that $COL1 is a legal column name + // and all column names must be in upper case. + +4. Group by, having, other clauses are ignored + +5. Expression columns, min(), max() are ignored + +6. All data is readonly. Only SELECTs permitted. +*/ + +// security - hide paths +if (!defined('ADODB_DIR')) die(); + +if (! defined("_ADODB_TEXT_LAYER")) { + define("_ADODB_TEXT_LAYER", 1 ); + +// for sorting in _query() +function adodb_cmp($a, $b) { + if ($a[0] == $b[0]) return 0; + return ($a[0] < $b[0]) ? -1 : 1; +} +// for sorting in _query() +function adodb_cmpr($a, $b) { + if ($a[0] == $b[0]) return 0; + return ($a[0] > $b[0]) ? -1 : 1; +} +class ADODB_text extends ADOConnection { + var $databaseType = 'text'; + + var $_origarray; // original data + var $_types; + var $_proberows = 8; + var $_colnames; + var $_skiprow1=false; + var $readOnly = true; + var $hasTransactions = false; + + var $_rezarray; + var $_reznames; + var $_reztypes; + + function __construct() + { + } + + function RSRecordCount() + { + if (!empty($this->_rezarray)) return sizeof($this->_rezarray); + + return sizeof($this->_origarray); + } + + function _insertid() + { + return false; + } + + function _affectedrows() + { + return false; + } + + // returns true or false + function PConnect(&$array, $types = false, $colnames = false) + { + return $this->Connect($array, $types, $colnames); + } + // returns true or false + function Connect(&$array, $types = false, $colnames = false) + { + if (is_string($array) and $array === 'iluvphplens') return 'me2'; + + if (!$array) { + $this->_origarray = false; + return true; + } + $row = $array[0]; + $cols = sizeof($row); + + + if ($colnames) $this->_colnames = $colnames; + else { + $this->_colnames = $array[0]; + $this->_skiprow1 = true; + } + if (!$types) { + // probe and guess the type + $types = array(); + $firstrow = true; + if ($this->_proberows > sizeof($array)) $max = sizeof($array); + else $max = $this->_proberows; + for ($j=($this->_skiprow1)?1:0;$j < $max; $j++) { + $row = $array[$j]; + if (!$row) break; + $i = -1; + foreach($row as $v) { + $i += 1; + //print " ($i ".$types[$i]. "$v) "; + $v = trim($v); + if (!preg_match('/^[+-]{0,1}[0-9\.]+$/',$v)) { + $types[$i] = 'C'; // once C, always C + continue; + } + if (isset($types[$i]) && $types[$i]=='C') continue; + if ($firstrow) { + // If empty string, we presume is character + // test for integer for 1st row only + // after that it is up to testing other rows to prove + // that it is not an integer + if (strlen($v) == 0) $types[0] = 'C'; + if (strpos($v,'.') !== false) $types[0] = 'N'; + else $types[$i] = 'I'; + continue; + } + + if (strpos($v,'.') !== false) $types[$i] = 'N'; + + } + $firstrow = false; + } + } + //print_r($types); + $this->_origarray = $array; + $this->_types = $types; + return true; + } + + + + // returns queryID or false + // We presume that the select statement is on the same table (what else?), + // with the only difference being the order by. + //You can filter by using $eval and each clause is stored in $arr .eg. $arr[1] == 'name' + // also supports SELECT [DISTINCT] COL FROM ... -- only 1 col supported + function _query($sql,$input_arr,$eval=false) + { + if ($this->_origarray === false) return false; + + $eval = $this->evalAll; + $usql = strtoupper(trim($sql)); + $usql = preg_replace("/[\t\n\r]/",' ',$usql); + $usql = preg_replace('/ *BY/i',' BY',strtoupper($usql)); + + $eregword ='([A-Z_0-9]*)'; + //print "
$sql $eval "; + if ($eval) { + $i = 0; + foreach($this->_colnames as $n) { + $n = strtoupper(trim($n)); + $eval = str_replace("\$$n","\$arr[$i]",$eval); + + $i += 1; + } + + $i = 0; + $eval = "\$rez=($eval);"; + //print "

Eval string = $eval

"; + $where_arr = array(); + + reset($this->_origarray); + while (list($k_arr,$arr) = each($this->_origarray)) { + + if ($i == 0 && $this->_skiprow1) + $where_arr[] = $arr; + else { + eval($eval); + //print " $i: result=$rez arr[0]={$arr[0]} arr[1]={$arr[1]}
\n "; + if ($rez) $where_arr[] = $arr; + } + $i += 1; + } + $this->_rezarray = $where_arr; + }else + $where_arr = $this->_origarray; + + // THIS PROJECTION CODE ONLY WORKS FOR 1 COLUMN, + // OTHERWISE IT RETURNS ALL COLUMNS + if (substr($usql,0,7) == 'SELECT ') { + $at = strpos($usql,' FROM '); + $sel = trim(substr($usql,7,$at-7)); + + $distinct = false; + if (substr($sel,0,8) == 'DISTINCT') { + $distinct = true; + $sel = trim(substr($sel,8,$at)); + } + + // $sel holds the selection clause, comma delimited + // currently we only project if one column is involved + // this is to support popups in PHPLens + if (strpos(',',$sel)===false) { + $colarr = array(); + + preg_match("/$eregword/",$sel,$colarr); + $col = $colarr[1]; + $i = 0; + $n = ''; + reset($this->_colnames); + while (list($k_n,$n) = each($this->_colnames)) { + + if ($col == strtoupper(trim($n))) break; + $i += 1; + } + + if ($n && $col) { + $distarr = array(); + $projarray = array(); + $projtypes = array($this->_types[$i]); + $projnames = array($n); + + reset($where_arr); + while (list($k_a,$a) = each($where_arr)) { + if ($i == 0 && $this->_skiprow1) { + $projarray[] = array($n); + continue; + } + + if ($distinct) { + $v = strtoupper($a[$i]); + if (! $distarr[$v]) { + $projarray[] = array($a[$i]); + $distarr[$v] = 1; + } + } else + $projarray[] = array($a[$i]); + + } //foreach + //print_r($projarray); + } + } // check 1 column in projection + } // is SELECT + + if (empty($projarray)) { + $projtypes = $this->_types; + $projarray = $where_arr; + $projnames = $this->_colnames; + } + $this->_rezarray = $projarray; + $this->_reztypes = $projtypes; + $this->_reznames = $projnames; + + + $pos = strpos($usql,' ORDER BY '); + if ($pos === false) return $this; + $orderby = trim(substr($usql,$pos+10)); + + preg_match("/$eregword/",$orderby,$arr); + if (sizeof($arr) < 2) return $this; // actually invalid sql + $col = $arr[1]; + $at = (integer) $col; + if ($at == 0) { + $i = 0; + reset($projnames); + while (list($k_n,$n) = each($projnames)) { + if (strtoupper(trim($n)) == $col) { + $at = $i+1; + break; + } + $i += 1; + } + } + + if ($at <= 0 || $at > sizeof($projarray[0])) return $this; // cannot find sort column + $at -= 1; + + // generate sort array consisting of (sortval1, row index1) (sortval2, row index2)... + $sorta = array(); + $t = $projtypes[$at]; + $num = ($t == 'I' || $t == 'N'); + for ($i=($this->_skiprow1)?1:0, $max = sizeof($projarray); $i < $max; $i++) { + $row = $projarray[$i]; + $val = ($num)?(float)$row[$at]:$row[$at]; + $sorta[]=array($val,$i); + } + + // check for desc sort + $orderby = substr($orderby,strlen($col)+1); + $arr == array(); + preg_match('/([A-Z_0-9]*)/i',$orderby,$arr); + + if (trim($arr[1]) == 'DESC') $sortf = 'adodb_cmpr'; + else $sortf = 'adodb_cmp'; + + // hasta la sorta babe + usort($sorta, $sortf); + + // rearrange original array + $arr2 = array(); + if ($this->_skiprow1) $arr2[] = $projarray[0]; + foreach($sorta as $v) { + $arr2[] = $projarray[$v[1]]; + } + + $this->_rezarray = $arr2; + return $this; + } + + /* Returns: the last error message from previous database operation */ + function ErrorMsg() + { + return ''; + } + + /* Returns: the last error number from previous database operation */ + function ErrorNo() + { + return 0; + } + + // returns true or false + function _close() + { + } + + +} + +/*-------------------------------------------------------------------------------------- + Class Name: Recordset +--------------------------------------------------------------------------------------*/ + + +class ADORecordSet_text extends ADORecordSet_array +{ + + var $databaseType = "text"; + + function __construct(&$conn,$mode=false) + { + parent::__construct(); + $this->InitArray($conn->_rezarray,$conn->_reztypes,$conn->_reznames); + $conn->_rezarray = false; + } + +} // class ADORecordSet_text + + +} // defined diff --git a/app/vendor/adodb/adodb-php/drivers/adodb-vfp.inc.php b/app/vendor/adodb/adodb-php/drivers/adodb-vfp.inc.php new file mode 100644 index 000000000..840c9c109 --- /dev/null +++ b/app/vendor/adodb/adodb-php/drivers/adodb-vfp.inc.php @@ -0,0 +1,103 @@ +replaceQuote,$s))."'"; + return "'".$s."'"; + } + + + // TOP requires ORDER BY for VFP + function SelectLimit($sql,$nrows=-1,$offset=-1, $inputarr=false,$secs2cache=0) + { + $this->hasTop = preg_match('/ORDER[ \t\r\n]+BY/is',$sql) ? 'top' : false; + $ret = ADOConnection::SelectLimit($sql,$nrows,$offset,$inputarr,$secs2cache); + return $ret; + } + + + +}; + + +class ADORecordSet_vfp extends ADORecordSet_odbc { + + var $databaseType = "vfp"; + + + function __construct($id,$mode=false) + { + return parent::__construct($id,$mode); + } + + function MetaType($t, $len = -1, $fieldobj = false) + { + if (is_object($t)) { + $fieldobj = $t; + $t = $fieldobj->type; + $len = $fieldobj->max_length; + } + switch (strtoupper($t)) { + case 'C': + if ($len <= $this->blobSize) return 'C'; + case 'M': + return 'X'; + + case 'D': return 'D'; + + case 'T': return 'T'; + + case 'L': return 'L'; + + case 'I': return 'I'; + + default: return 'N'; + } + } +} + +} //define diff --git a/app/vendor/adodb/adodb-php/lang/adodb-ar.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-ar.inc.php new file mode 100644 index 000000000..0b8f12ff8 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-ar.inc.php @@ -0,0 +1,32 @@ + +$ADODB_LANG_ARRAY = array ( + 'LANG' => 'ar', + DB_ERROR => 'خطأ غير محدد', + DB_ERROR_ALREADY_EXISTS => 'موجود مسبقا', + DB_ERROR_CANNOT_CREATE => 'لا يمكن إنشاء', + DB_ERROR_CANNOT_DELETE => 'لا يمكن حذف', + DB_ERROR_CANNOT_DROP => 'لا يمكن حذف', + DB_ERROR_CONSTRAINT => 'عملية إدخال ممنوعة', + DB_ERROR_DIVZERO => 'عملية التقسيم على صفر', + DB_ERROR_INVALID => 'غير صحيح', + DB_ERROR_INVALID_DATE => 'صيغة وقت أو تاريخ غير صحيحة', + DB_ERROR_INVALID_NUMBER => 'صيغة رقم غير صحيحة', + DB_ERROR_MISMATCH => 'غير متطابق', + DB_ERROR_NODBSELECTED => 'لم يتم إختيار قاعدة البيانات بعد', + DB_ERROR_NOSUCHFIELD => 'ليس هنالك حقل بهذا الاسم', + DB_ERROR_NOSUCHTABLE => 'ليس هنالك جدول بهذا الاسم', + DB_ERROR_NOT_CAPABLE => 'قاعدة البيانات المرتبط بها غير قادرة', + DB_ERROR_NOT_FOUND => 'لم يتم إيجاده', + DB_ERROR_NOT_LOCKED => 'غير مقفول', + DB_ERROR_SYNTAX => 'خطأ في الصيغة', + DB_ERROR_UNSUPPORTED => 'غير مدعوم', + DB_ERROR_VALUE_COUNT_ON_ROW => 'عدد القيم في السجل', + DB_ERROR_INVALID_DSN => 'DSN غير صحيح', + DB_ERROR_CONNECT_FAILED => 'فشل عملية الإتصال', + 0 => 'ليس هنالك أخطاء', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'البيانات المزودة غير كافية', + DB_ERROR_EXTENSION_NOT_FOUND=> 'لم يتم إيجاد الإضافة المتعلقة', + DB_ERROR_NOSUCHDB => 'ليس هنالك قاعدة بيانات بهذا الاسم', + DB_ERROR_ACCESS_VIOLATION => 'سماحيات غير كافية' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-bg.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-bg.inc.php new file mode 100644 index 000000000..07069b421 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-bg.inc.php @@ -0,0 +1,36 @@ + +*/ + +$ADODB_LANG_ARRAY = array ( + 'LANG' => 'bg', + DB_ERROR => 'неизвестна грешка', + DB_ERROR_ALREADY_EXISTS => 'вече съществува', + DB_ERROR_CANNOT_CREATE => 'не може да бъде създадена', + DB_ERROR_CANNOT_DELETE => 'не може да бъде изтрита', + DB_ERROR_CANNOT_DROP => 'не може да бъде унищожена', + DB_ERROR_CONSTRAINT => 'нарушено условие', + DB_ERROR_DIVZERO => 'деление на нула', + DB_ERROR_INVALID => 'неправилно', + DB_ERROR_INVALID_DATE => 'некоректна дата или час', + DB_ERROR_INVALID_NUMBER => 'невалиден номер', + DB_ERROR_MISMATCH => 'погрешна употреба', + DB_ERROR_NODBSELECTED => 'не е избрана база данни', + DB_ERROR_NOSUCHFIELD => 'несъществуващо поле', + DB_ERROR_NOSUCHTABLE => 'несъществуваща таблица', + DB_ERROR_NOT_CAPABLE => 'DB backend not capable', + DB_ERROR_NOT_FOUND => 'не е намерена', + DB_ERROR_NOT_LOCKED => 'не е заключена', + DB_ERROR_SYNTAX => 'грешен синтаксис', + DB_ERROR_UNSUPPORTED => 'не се поддържа', + DB_ERROR_VALUE_COUNT_ON_ROW => 'некоректен брой колони в реда', + DB_ERROR_INVALID_DSN => 'невалиден DSN', + DB_ERROR_CONNECT_FAILED => 'връзката не може да бъде осъществена', + 0 => 'няма грешки', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'предоставените данни са недостатъчни', + DB_ERROR_EXTENSION_NOT_FOUND=> 'разширението не е намерено', + DB_ERROR_NOSUCHDB => 'несъществуваща база данни', + DB_ERROR_ACCESS_VIOLATION => 'нямате достатъчно права' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-ca.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-ca.inc.php new file mode 100644 index 000000000..adbafac98 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-ca.inc.php @@ -0,0 +1,33 @@ + 'ca', + DB_ERROR => 'error desconegut', + DB_ERROR_ALREADY_EXISTS => 'ja existeix', + DB_ERROR_CANNOT_CREATE => 'no es pot crear', + DB_ERROR_CANNOT_DELETE => 'no es pot esborrar', + DB_ERROR_CANNOT_DROP => 'no es pot eliminar', + DB_ERROR_CONSTRAINT => 'violació de constraint', + DB_ERROR_DIVZERO => 'divisió per zero', + DB_ERROR_INVALID => 'no és vàlid', + DB_ERROR_INVALID_DATE => 'la data o l\'hora no són vàlides', + DB_ERROR_INVALID_NUMBER => 'el nombre no és vàlid', + DB_ERROR_MISMATCH => 'no hi ha coincidència', + DB_ERROR_NODBSELECTED => 'cap base de dades seleccionada', + DB_ERROR_NOSUCHFIELD => 'camp inexistent', + DB_ERROR_NOSUCHTABLE => 'taula inexistent', + DB_ERROR_NOT_CAPABLE => 'l\'execució secundària de DB no pot', + DB_ERROR_NOT_FOUND => 'no trobat', + DB_ERROR_NOT_LOCKED => 'no blocat', + DB_ERROR_SYNTAX => 'error de sintaxi', + DB_ERROR_UNSUPPORTED => 'no suportat', + DB_ERROR_VALUE_COUNT_ON_ROW => 'el nombre de columnes no coincideix amb el nombre de valors en la fila', + DB_ERROR_INVALID_DSN => 'el DSN no és vàlid', + DB_ERROR_CONNECT_FAILED => 'connexió fallida', + 0 => 'cap error', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'les dades subministrades són insuficients', + DB_ERROR_EXTENSION_NOT_FOUND=> 'extensió no trobada', + DB_ERROR_NOSUCHDB => 'base de dades inexistent', + DB_ERROR_ACCESS_VIOLATION => 'permisos insuficients' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-cn.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-cn.inc.php new file mode 100644 index 000000000..9c9734130 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-cn.inc.php @@ -0,0 +1,33 @@ + 'cn', + DB_ERROR => '未知错误', + DB_ERROR_ALREADY_EXISTS => '已经存在', + DB_ERROR_CANNOT_CREATE => '不能创建', + DB_ERROR_CANNOT_DELETE => '不能删除', + DB_ERROR_CANNOT_DROP => '不能丢弃', + DB_ERROR_CONSTRAINT => '约束限制', + DB_ERROR_DIVZERO => '被0除', + DB_ERROR_INVALID => '无效', + DB_ERROR_INVALID_DATE => '无效的日期或者时间', + DB_ERROR_INVALID_NUMBER => '无效的数字', + DB_ERROR_MISMATCH => '不匹配', + DB_ERROR_NODBSELECTED => '没有数据库被选择', + DB_ERROR_NOSUCHFIELD => '没有相应的字段', + DB_ERROR_NOSUCHTABLE => '没有相应的表', + DB_ERROR_NOT_CAPABLE => '数据库后台不兼容', + DB_ERROR_NOT_FOUND => '没有发现', + DB_ERROR_NOT_LOCKED => '没有被锁定', + DB_ERROR_SYNTAX => '语法错误', + DB_ERROR_UNSUPPORTED => '不支持', + DB_ERROR_VALUE_COUNT_ON_ROW => '在行上累计值', + DB_ERROR_INVALID_DSN => '无效的数据源 (DSN)', + DB_ERROR_CONNECT_FAILED => '连接失败', + 0 => '没有错误', // DB_OK + DB_ERROR_NEED_MORE_DATA => '提供的数据不能符合要求', + DB_ERROR_EXTENSION_NOT_FOUND=> '扩展没有被发现', + DB_ERROR_NOSUCHDB => '没有相应的数据库', + DB_ERROR_ACCESS_VIOLATION => '没有合适的权限' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-cz.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-cz.inc.php new file mode 100644 index 000000000..d79d7142f --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-cz.inc.php @@ -0,0 +1,35 @@ + + +$ADODB_LANG_ARRAY = array ( + 'LANG' => 'cz', + DB_ERROR => 'neznámá chyba', + DB_ERROR_ALREADY_EXISTS => 'ji? existuje', + DB_ERROR_CANNOT_CREATE => 'nelze vytvo?it', + DB_ERROR_CANNOT_DELETE => 'nelze smazat', + DB_ERROR_CANNOT_DROP => 'nelze odstranit', + DB_ERROR_CONSTRAINT => 'poru?ení omezující podmínky', + DB_ERROR_DIVZERO => 'd?lení nulou', + DB_ERROR_INVALID => 'neplatné', + DB_ERROR_INVALID_DATE => 'neplatné datum nebo ?as', + DB_ERROR_INVALID_NUMBER => 'neplatné ?íslo', + DB_ERROR_MISMATCH => 'nesouhlasí', + DB_ERROR_NODBSELECTED => '?ádná databáze není vybrána', + DB_ERROR_NOSUCHFIELD => 'pole nenalezeno', + DB_ERROR_NOSUCHTABLE => 'tabulka nenalezena', + DB_ERROR_NOT_CAPABLE => 'nepodporováno', + DB_ERROR_NOT_FOUND => 'nenalezeno', + DB_ERROR_NOT_LOCKED => 'nezam?eno', + DB_ERROR_SYNTAX => 'syntaktická chyba', + DB_ERROR_UNSUPPORTED => 'nepodporováno', + DB_ERROR_VALUE_COUNT_ON_ROW => '', + DB_ERROR_INVALID_DSN => 'neplatné DSN', + DB_ERROR_CONNECT_FAILED => 'p?ipojení selhalo', + 0 => 'bez chyb', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'málo zdrojových dat', + DB_ERROR_EXTENSION_NOT_FOUND=> 'roz?í?ení nenalezeno', + DB_ERROR_NOSUCHDB => 'databáze neexistuje', + DB_ERROR_ACCESS_VIOLATION => 'nedostate?ná práva' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-da.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-da.inc.php new file mode 100644 index 000000000..14e720b85 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-da.inc.php @@ -0,0 +1,32 @@ + 'da', + DB_ERROR => 'ukendt fejl', + DB_ERROR_ALREADY_EXISTS => 'eksisterer allerede', + DB_ERROR_CANNOT_CREATE => 'kan ikke oprette', + DB_ERROR_CANNOT_DELETE => 'kan ikke slette', + DB_ERROR_CANNOT_DROP => 'kan ikke droppe', + DB_ERROR_CONSTRAINT => 'begrænsning krænket', + DB_ERROR_DIVZERO => 'division med nul', + DB_ERROR_INVALID => 'ugyldig', + DB_ERROR_INVALID_DATE => 'ugyldig dato eller klokkeslet', + DB_ERROR_INVALID_NUMBER => 'ugyldigt tal', + DB_ERROR_MISMATCH => 'mismatch', + DB_ERROR_NODBSELECTED => 'ingen database valgt', + DB_ERROR_NOSUCHFIELD => 'felt findes ikke', + DB_ERROR_NOSUCHTABLE => 'tabel findes ikke', + DB_ERROR_NOT_CAPABLE => 'DB backend opgav', + DB_ERROR_NOT_FOUND => 'ikke fundet', + DB_ERROR_NOT_LOCKED => 'ikke låst', + DB_ERROR_SYNTAX => 'syntaksfejl', + DB_ERROR_UNSUPPORTED => 'ikke understøttet', + DB_ERROR_VALUE_COUNT_ON_ROW => 'resulterende antal felter svarer ikke til forespørgslens antal felter', + DB_ERROR_INVALID_DSN => 'ugyldig DSN', + DB_ERROR_CONNECT_FAILED => 'tilslutning mislykkedes', + 0 => 'ingen fejl', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'utilstrækkelige data angivet', + DB_ERROR_EXTENSION_NOT_FOUND=> 'udvidelse ikke fundet', + DB_ERROR_NOSUCHDB => 'database ikke fundet', + DB_ERROR_ACCESS_VIOLATION => 'utilstrækkelige rettigheder' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-de.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-de.inc.php new file mode 100644 index 000000000..dca4ffefe --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-de.inc.php @@ -0,0 +1,32 @@ + +$ADODB_LANG_ARRAY = array ( + 'LANG' => 'de', + DB_ERROR => 'Unbekannter Fehler', + DB_ERROR_ALREADY_EXISTS => 'existiert bereits', + DB_ERROR_CANNOT_CREATE => 'kann nicht erstellen', + DB_ERROR_CANNOT_DELETE => 'kann nicht löschen', + DB_ERROR_CANNOT_DROP => 'Tabelle oder Index konnte nicht gelöscht werden', + DB_ERROR_CONSTRAINT => 'Constraint Verletzung', + DB_ERROR_DIVZERO => 'Division durch Null', + DB_ERROR_INVALID => 'ungültig', + DB_ERROR_INVALID_DATE => 'ungültiges Datum oder Zeit', + DB_ERROR_INVALID_NUMBER => 'ungültige Zahl', + DB_ERROR_MISMATCH => 'Unverträglichkeit', + DB_ERROR_NODBSELECTED => 'keine Dantebank ausgewählt', + DB_ERROR_NOSUCHFIELD => 'Feld nicht vorhanden', + DB_ERROR_NOSUCHTABLE => 'Tabelle nicht vorhanden', + DB_ERROR_NOT_CAPABLE => 'Funktion nicht installiert', + DB_ERROR_NOT_FOUND => 'nicht gefunden', + DB_ERROR_NOT_LOCKED => 'nicht gesperrt', + DB_ERROR_SYNTAX => 'Syntaxfehler', + DB_ERROR_UNSUPPORTED => 'nicht Unterstützt', + DB_ERROR_VALUE_COUNT_ON_ROW => 'Anzahl der zurückgelieferten Felder entspricht nicht der Anzahl der Felder in der Abfrage', + DB_ERROR_INVALID_DSN => 'ungültiger DSN', + DB_ERROR_CONNECT_FAILED => 'Verbindung konnte nicht hergestellt werden', + 0 => 'kein Fehler', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'Nicht genügend Daten geliefert', + DB_ERROR_EXTENSION_NOT_FOUND=> 'erweiterung nicht gefunden', + DB_ERROR_NOSUCHDB => 'keine Datenbank', + DB_ERROR_ACCESS_VIOLATION => 'ungenügende Rechte' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-en.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-en.inc.php new file mode 100644 index 000000000..05828554c --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-en.inc.php @@ -0,0 +1,35 @@ + 'en', + DB_ERROR => 'unknown error', + DB_ERROR_ALREADY_EXISTS => 'already exists', + DB_ERROR_CANNOT_CREATE => 'can not create', + DB_ERROR_CANNOT_DELETE => 'can not delete', + DB_ERROR_CANNOT_DROP => 'can not drop', + DB_ERROR_CONSTRAINT => 'constraint violation', + DB_ERROR_DIVZERO => 'division by zero', + DB_ERROR_INVALID => 'invalid', + DB_ERROR_INVALID_DATE => 'invalid date or time', + DB_ERROR_INVALID_NUMBER => 'invalid number', + DB_ERROR_MISMATCH => 'mismatch', + DB_ERROR_NODBSELECTED => 'no database selected', + DB_ERROR_NOSUCHFIELD => 'no such field', + DB_ERROR_NOSUCHTABLE => 'no such table', + DB_ERROR_NOT_CAPABLE => 'DB backend not capable', + DB_ERROR_NOT_FOUND => 'not found', + DB_ERROR_NOT_LOCKED => 'not locked', + DB_ERROR_SYNTAX => 'syntax error', + DB_ERROR_UNSUPPORTED => 'not supported', + DB_ERROR_VALUE_COUNT_ON_ROW => 'value count on row', + DB_ERROR_INVALID_DSN => 'invalid DSN', + DB_ERROR_CONNECT_FAILED => 'connect failed', + 0 => 'no error', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'insufficient data supplied', + DB_ERROR_EXTENSION_NOT_FOUND=> 'extension not found', + DB_ERROR_NOSUCHDB => 'no such database', + DB_ERROR_ACCESS_VIOLATION => 'insufficient permissions', + DB_ERROR_DEADLOCK => 'deadlock detected', + DB_ERROR_STATEMENT_TIMEOUT => 'statement timeout', + DB_ERROR_SERIALIZATION_FAILURE => 'could not serialize access' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-eo.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-eo.inc.php new file mode 100644 index 000000000..baa589c1c --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-eo.inc.php @@ -0,0 +1,34 @@ + 'eo', + DB_ERROR => 'nekonata eraro', + DB_ERROR_ALREADY_EXISTS => 'jam ekzistas', + DB_ERROR_CANNOT_CREATE => 'maleblas krei', + DB_ERROR_CANNOT_DELETE => 'maleblas elimini', + DB_ERROR_CANNOT_DROP => 'maleblas elimini (drop)', + DB_ERROR_CONSTRAINT => 'rompo de kondiĉoj de provo', + DB_ERROR_DIVZERO => 'divido per 0 (nul)', + DB_ERROR_INVALID => 'malregule', + DB_ERROR_INVALID_DATE => 'malregula dato kaj tempo', + DB_ERROR_INVALID_NUMBER => 'malregula nombro', + DB_ERROR_MISMATCH => 'eraro', + DB_ERROR_NODBSELECTED => 'datumbazo ne elektita', + DB_ERROR_NOSUCHFIELD => 'ne ekzistas kampo', + DB_ERROR_NOSUCHTABLE => 'ne ekzistas tabelo', + DB_ERROR_NOT_CAPABLE => 'DBMS ne povas', + DB_ERROR_NOT_FOUND => 'ne trovita', + DB_ERROR_NOT_LOCKED => 'ne blokita', + DB_ERROR_SYNTAX => 'sintaksa eraro', + DB_ERROR_UNSUPPORTED => 'ne apogata', + DB_ERROR_VALUE_COUNT_ON_ROW => 'nombrilo de valoroj en linio', + DB_ERROR_INVALID_DSN => 'malregula DSN-o', + DB_ERROR_CONNECT_FAILED => 'konekto malsukcesa', + 0 => 'ĉio bone', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'ne sufiĉe da datumo', + DB_ERROR_EXTENSION_NOT_FOUND=> 'etendo ne trovita', + DB_ERROR_NOSUCHDB => 'datumbazo ne ekzistas', + DB_ERROR_ACCESS_VIOLATION => 'ne sufiĉe da rajto por atingo' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-es.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-es.inc.php new file mode 100644 index 000000000..a80a64415 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-es.inc.php @@ -0,0 +1,32 @@ + +$ADODB_LANG_ARRAY = array ( + 'LANG' => 'es', + DB_ERROR => 'error desconocido', + DB_ERROR_ALREADY_EXISTS => 'ya existe', + DB_ERROR_CANNOT_CREATE => 'imposible crear', + DB_ERROR_CANNOT_DELETE => 'imposible borrar', + DB_ERROR_CANNOT_DROP => 'imposible hacer drop', + DB_ERROR_CONSTRAINT => 'violacion de constraint', + DB_ERROR_DIVZERO => 'division por cero', + DB_ERROR_INVALID => 'invalido', + DB_ERROR_INVALID_DATE => 'fecha u hora invalida', + DB_ERROR_INVALID_NUMBER => 'numero invalido', + DB_ERROR_MISMATCH => 'error', + DB_ERROR_NODBSELECTED => 'no hay base de datos seleccionada', + DB_ERROR_NOSUCHFIELD => 'campo invalido', + DB_ERROR_NOSUCHTABLE => 'tabla no existe', + DB_ERROR_NOT_CAPABLE => 'capacidad invalida para esta DB', + DB_ERROR_NOT_FOUND => 'no encontrado', + DB_ERROR_NOT_LOCKED => 'no bloqueado', + DB_ERROR_SYNTAX => 'error de sintaxis', + DB_ERROR_UNSUPPORTED => 'no soportado', + DB_ERROR_VALUE_COUNT_ON_ROW => 'la cantidad de columnas no corresponden a la cantidad de valores', + DB_ERROR_INVALID_DSN => 'DSN invalido', + DB_ERROR_CONNECT_FAILED => 'fallo la conexion', + 0 => 'sin error', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'insuficientes datos', + DB_ERROR_EXTENSION_NOT_FOUND=> 'extension no encontrada', + DB_ERROR_NOSUCHDB => 'base de datos no encontrada', + DB_ERROR_ACCESS_VIOLATION => 'permisos insuficientes' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-fa.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-fa.inc.php new file mode 100644 index 000000000..7fa46183e --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-fa.inc.php @@ -0,0 +1,34 @@ + */ + +$ADODB_LANG_ARRAY = array ( + 'LANG' => 'fa', + DB_ERROR => 'خطای ناشناخته', + DB_ERROR_ALREADY_EXISTS => 'وجود دارد', + DB_ERROR_CANNOT_CREATE => 'امکان create وجود ندارد', + DB_ERROR_CANNOT_DELETE => 'امکان حذف وجود ندارد', + DB_ERROR_CANNOT_DROP => 'امکان drop وجود ندارد', + DB_ERROR_CONSTRAINT => 'نقض شرط', + DB_ERROR_DIVZERO => 'تقسیم بر صفر', + DB_ERROR_INVALID => 'نامعتبر', + DB_ERROR_INVALID_DATE => 'زمان یا تاریخ نامعتبر', + DB_ERROR_INVALID_NUMBER => 'عدد نامعتبر', + DB_ERROR_MISMATCH => 'عدم مطابقت', + DB_ERROR_NODBSELECTED => 'بانک اطلاعاتی انتخاب نشده است', + DB_ERROR_NOSUCHFIELD => 'چنین ستونی وجود ندارد', + DB_ERROR_NOSUCHTABLE => 'چنین جدولی وجود ندارد', + DB_ERROR_NOT_CAPABLE => 'backend بانک اطلاعاتی قادر نیست', + DB_ERROR_NOT_FOUND => 'پیدا نشد', + DB_ERROR_NOT_LOCKED => 'قفل نشده', + DB_ERROR_SYNTAX => 'خطای دستوری', + DB_ERROR_UNSUPPORTED => 'پشتیبانی نمی شود', + DB_ERROR_VALUE_COUNT_ON_ROW => 'شمارش مقادیر روی ردیف', + DB_ERROR_INVALID_DSN => 'DSN نامعتبر', + DB_ERROR_CONNECT_FAILED => 'ارتباط برقرار نشد', + 0 => 'بدون خطا', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'داده ناکافی است', + DB_ERROR_EXTENSION_NOT_FOUND=> 'extension پیدا نشد', + DB_ERROR_NOSUCHDB => 'چنین بانک اطلاعاتی وجود ندارد', + DB_ERROR_ACCESS_VIOLATION => 'حق دسترسی ناکافی' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-fr.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-fr.inc.php new file mode 100644 index 000000000..620196b43 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-fr.inc.php @@ -0,0 +1,32 @@ + 'fr', + DB_ERROR => 'erreur inconnue', + DB_ERROR_ALREADY_EXISTS => 'existe déjà', + DB_ERROR_CANNOT_CREATE => 'création impossible', + DB_ERROR_CANNOT_DELETE => 'effacement impossible', + DB_ERROR_CANNOT_DROP => 'suppression impossible', + DB_ERROR_CONSTRAINT => 'violation de contrainte', + DB_ERROR_DIVZERO => 'division par zéro', + DB_ERROR_INVALID => 'invalide', + DB_ERROR_INVALID_DATE => 'date ou heure invalide', + DB_ERROR_INVALID_NUMBER => 'nombre invalide', + DB_ERROR_MISMATCH => 'erreur de concordance', + DB_ERROR_NODBSELECTED => 'pas de base de données sélectionnée', + DB_ERROR_NOSUCHFIELD => 'nom de colonne invalide', + DB_ERROR_NOSUCHTABLE => 'table ou vue inexistante', + DB_ERROR_NOT_CAPABLE => 'fonction optionnelle non installée', + DB_ERROR_NOT_FOUND => 'pas trouvé', + DB_ERROR_NOT_LOCKED => 'non verrouillé', + DB_ERROR_SYNTAX => 'erreur de syntaxe', + DB_ERROR_UNSUPPORTED => 'non supporté', + DB_ERROR_VALUE_COUNT_ON_ROW => 'valeur insérée trop grande pour colonne', + DB_ERROR_INVALID_DSN => 'DSN invalide', + DB_ERROR_CONNECT_FAILED => 'échec à la connexion', + 0 => "pas d'erreur", // DB_OK + DB_ERROR_NEED_MORE_DATA => 'données fournies insuffisantes', + DB_ERROR_EXTENSION_NOT_FOUND=> 'extension non trouvée', + DB_ERROR_NOSUCHDB => 'base de données inconnue', + DB_ERROR_ACCESS_VIOLATION => 'droits insuffisants' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-hu.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-hu.inc.php new file mode 100644 index 000000000..49357ce22 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-hu.inc.php @@ -0,0 +1,33 @@ + +$ADODB_LANG_ARRAY = array ( + 'LANG' => 'hu', + DB_ERROR => 'ismeretlen hiba', + DB_ERROR_ALREADY_EXISTS => 'már létezik', + DB_ERROR_CANNOT_CREATE => 'nem sikerült létrehozni', + DB_ERROR_CANNOT_DELETE => 'nem sikerült törölni', + DB_ERROR_CANNOT_DROP => 'nem sikerült eldobni', + DB_ERROR_CONSTRAINT => 'szabályok megszegése', + DB_ERROR_DIVZERO => 'osztás nullával', + DB_ERROR_INVALID => 'érvénytelen', + DB_ERROR_INVALID_DATE => 'érvénytelen dátum vagy idő', + DB_ERROR_INVALID_NUMBER => 'érvénytelen szám', + DB_ERROR_MISMATCH => 'nem megfelelő', + DB_ERROR_NODBSELECTED => 'nincs kiválasztott adatbázis', + DB_ERROR_NOSUCHFIELD => 'nincs ilyen mező', + DB_ERROR_NOSUCHTABLE => 'nincs ilyen tábla', + DB_ERROR_NOT_CAPABLE => 'DB backend nem támogatja', + DB_ERROR_NOT_FOUND => 'nem található', + DB_ERROR_NOT_LOCKED => 'nincs lezárva', + DB_ERROR_SYNTAX => 'szintaktikai hiba', + DB_ERROR_UNSUPPORTED => 'nem támogatott', + DB_ERROR_VALUE_COUNT_ON_ROW => 'soron végzett érték számlálás', + DB_ERROR_INVALID_DSN => 'hibás DSN', + DB_ERROR_CONNECT_FAILED => 'sikertelen csatlakozás', + 0 => 'nincs hiba', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'túl kevés az adat', + DB_ERROR_EXTENSION_NOT_FOUND=> 'bővítmény nem található', + DB_ERROR_NOSUCHDB => 'nincs ilyen adatbázis', + DB_ERROR_ACCESS_VIOLATION => 'nincs jogosultság' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-it.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-it.inc.php new file mode 100644 index 000000000..80524e1d3 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-it.inc.php @@ -0,0 +1,33 @@ + 'it', + DB_ERROR => 'errore sconosciuto', + DB_ERROR_ALREADY_EXISTS => 'esiste già', + DB_ERROR_CANNOT_CREATE => 'non posso creare', + DB_ERROR_CANNOT_DELETE => 'non posso cancellare', + DB_ERROR_CANNOT_DROP => 'non posso eliminare', + DB_ERROR_CONSTRAINT => 'violazione constraint', + DB_ERROR_DIVZERO => 'divisione per zero', + DB_ERROR_INVALID => 'non valido', + DB_ERROR_INVALID_DATE => 'data od ora non valida', + DB_ERROR_INVALID_NUMBER => 'numero non valido', + DB_ERROR_MISMATCH => 'diversi', + DB_ERROR_NODBSELECTED => 'nessun database selezionato', + DB_ERROR_NOSUCHFIELD => 'nessun campo trovato', + DB_ERROR_NOSUCHTABLE => 'nessuna tabella trovata', + DB_ERROR_NOT_CAPABLE => 'DB backend non abilitato', + DB_ERROR_NOT_FOUND => 'non trovato', + DB_ERROR_NOT_LOCKED => 'non bloccato', + DB_ERROR_SYNTAX => 'errore di sintassi', + DB_ERROR_UNSUPPORTED => 'non supportato', + DB_ERROR_VALUE_COUNT_ON_ROW => 'valore inserito troppo grande per una colonna', + DB_ERROR_INVALID_DSN => 'DSN non valido', + DB_ERROR_CONNECT_FAILED => 'connessione fallita', + 0 => 'nessun errore', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'dati inseriti insufficienti', + DB_ERROR_EXTENSION_NOT_FOUND=> 'estensione non trovata', + DB_ERROR_NOSUCHDB => 'database non trovato', + DB_ERROR_ACCESS_VIOLATION => 'permessi insufficienti' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-nl.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-nl.inc.php new file mode 100644 index 000000000..43e3ee694 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-nl.inc.php @@ -0,0 +1,32 @@ + 'nl', + DB_ERROR => 'onbekende fout', + DB_ERROR_ALREADY_EXISTS => 'bestaat al', + DB_ERROR_CANNOT_CREATE => 'kan niet aanmaken', + DB_ERROR_CANNOT_DELETE => 'kan niet wissen', + DB_ERROR_CANNOT_DROP => 'kan niet verwijderen', + DB_ERROR_CONSTRAINT => 'constraint overtreding', + DB_ERROR_DIVZERO => 'poging tot delen door nul', + DB_ERROR_INVALID => 'ongeldig', + DB_ERROR_INVALID_DATE => 'ongeldige datum of tijd', + DB_ERROR_INVALID_NUMBER => 'ongeldig nummer', + DB_ERROR_MISMATCH => 'is incorrect', + DB_ERROR_NODBSELECTED => 'geen database geselecteerd', + DB_ERROR_NOSUCHFIELD => 'onbekend veld', + DB_ERROR_NOSUCHTABLE => 'onbekende tabel', + DB_ERROR_NOT_CAPABLE => 'database systeem is niet tot uitvoer in staat', + DB_ERROR_NOT_FOUND => 'niet gevonden', + DB_ERROR_NOT_LOCKED => 'niet vergrendeld', + DB_ERROR_SYNTAX => 'syntaxis fout', + DB_ERROR_UNSUPPORTED => 'niet ondersteund', + DB_ERROR_VALUE_COUNT_ON_ROW => 'waarde telling op rij', + DB_ERROR_INVALID_DSN => 'ongeldige DSN', + DB_ERROR_CONNECT_FAILED => 'connectie mislukt', + 0 => 'geen fout', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'onvoldoende data gegeven', + DB_ERROR_EXTENSION_NOT_FOUND=> 'extensie niet gevonden', + DB_ERROR_NOSUCHDB => 'onbekende database', + DB_ERROR_ACCESS_VIOLATION => 'onvoldoende rechten' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-pl.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-pl.inc.php new file mode 100644 index 000000000..ffa10e33e --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-pl.inc.php @@ -0,0 +1,34 @@ + + +$ADODB_LANG_ARRAY = array ( + 'LANG' => 'pl', + DB_ERROR => 'niezidentyfikowany błąd', + DB_ERROR_ALREADY_EXISTS => 'już istnieją', + DB_ERROR_CANNOT_CREATE => 'nie można stworzyć', + DB_ERROR_CANNOT_DELETE => 'nie można usunąć', + DB_ERROR_CANNOT_DROP => 'nie można porzucić', + DB_ERROR_CONSTRAINT => 'pogwałcenie uprawnień', + DB_ERROR_DIVZERO => 'dzielenie przez zero', + DB_ERROR_INVALID => 'błędny', + DB_ERROR_INVALID_DATE => 'błędna godzina lub data', + DB_ERROR_INVALID_NUMBER => 'błędny numer', + DB_ERROR_MISMATCH => 'niedopasowanie', + DB_ERROR_NODBSELECTED => 'baza danych nie została wybrana', + DB_ERROR_NOSUCHFIELD => 'nie znaleziono pola', + DB_ERROR_NOSUCHTABLE => 'nie znaleziono tabeli', + DB_ERROR_NOT_CAPABLE => 'nie zdolny', + DB_ERROR_NOT_FOUND => 'nie znaleziono', + DB_ERROR_NOT_LOCKED => 'nie zakmnięty', + DB_ERROR_SYNTAX => 'błąd składni', + DB_ERROR_UNSUPPORTED => 'nie obsługuje', + DB_ERROR_VALUE_COUNT_ON_ROW => 'wartość liczona w szeregu', + DB_ERROR_INVALID_DSN => 'błędny DSN', + DB_ERROR_CONNECT_FAILED => 'połączenie nie zostało zrealizowane', + 0 => 'brak błędów', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'niedostateczna ilość informacji', + DB_ERROR_EXTENSION_NOT_FOUND=> 'nie znaleziono rozszerzenia', + DB_ERROR_NOSUCHDB => 'nie znaleziono bazy', + DB_ERROR_ACCESS_VIOLATION => 'niedostateczne uprawnienia' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-pt-br.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-pt-br.inc.php new file mode 100644 index 000000000..9c687b060 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-pt-br.inc.php @@ -0,0 +1,34 @@ + 'pt-br', + DB_ERROR => 'erro desconhecido', + DB_ERROR_ALREADY_EXISTS => 'já existe', + DB_ERROR_CANNOT_CREATE => 'impossível criar', + DB_ERROR_CANNOT_DELETE => 'impossível excluír', + DB_ERROR_CANNOT_DROP => 'impossível remover', + DB_ERROR_CONSTRAINT => 'violação do confinamente', + DB_ERROR_DIVZERO => 'divisão por zero', + DB_ERROR_INVALID => 'inválido', + DB_ERROR_INVALID_DATE => 'data ou hora inválida', + DB_ERROR_INVALID_NUMBER => 'número inválido', + DB_ERROR_MISMATCH => 'erro', + DB_ERROR_NODBSELECTED => 'nenhum banco de dados selecionado', + DB_ERROR_NOSUCHFIELD => 'campo inválido', + DB_ERROR_NOSUCHTABLE => 'tabela inexistente', + DB_ERROR_NOT_CAPABLE => 'capacidade inválida para este BD', + DB_ERROR_NOT_FOUND => 'não encontrado', + DB_ERROR_NOT_LOCKED => 'não bloqueado', + DB_ERROR_SYNTAX => 'erro de sintaxe', + DB_ERROR_UNSUPPORTED => +'não suportado', + DB_ERROR_VALUE_COUNT_ON_ROW => 'a quantidade de colunas não corresponde ao de valores', + DB_ERROR_INVALID_DSN => 'DSN inválido', + DB_ERROR_CONNECT_FAILED => 'falha na conexão', + 0 => 'sem erro', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'dados insuficientes', + DB_ERROR_EXTENSION_NOT_FOUND=> 'extensão não encontrada', + DB_ERROR_NOSUCHDB => 'banco de dados não encontrado', + DB_ERROR_ACCESS_VIOLATION => 'permissão insuficiente' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-ro.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-ro.inc.php new file mode 100644 index 000000000..b6ddd3132 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-ro.inc.php @@ -0,0 +1,34 @@ + */ + +$ADODB_LANG_ARRAY = array ( + 'LANG' => 'ro', + DB_ERROR => 'eroare necunoscuta', + DB_ERROR_ALREADY_EXISTS => 'deja exista', + DB_ERROR_CANNOT_CREATE => 'nu se poate creea', + DB_ERROR_CANNOT_DELETE => 'nu se poate sterge', + DB_ERROR_CANNOT_DROP => 'nu se poate executa drop', + DB_ERROR_CONSTRAINT => 'violare de constrain', + DB_ERROR_DIVZERO => 'se divide la zero', + DB_ERROR_INVALID => 'invalid', + DB_ERROR_INVALID_DATE => 'data sau timp invalide', + DB_ERROR_INVALID_NUMBER => 'numar invalid', + DB_ERROR_MISMATCH => 'nepotrivire-mismatch', + DB_ERROR_NODBSELECTED => 'nu exista baza de date selectata', + DB_ERROR_NOSUCHFIELD => 'camp inexistent', + DB_ERROR_NOSUCHTABLE => 'tabela inexistenta', + DB_ERROR_NOT_CAPABLE => 'functie optionala neinstalata', + DB_ERROR_NOT_FOUND => 'negasit', + DB_ERROR_NOT_LOCKED => 'neblocat', + DB_ERROR_SYNTAX => 'eroare de sintaxa', + DB_ERROR_UNSUPPORTED => 'nu e suportat', + DB_ERROR_VALUE_COUNT_ON_ROW => 'valoare prea mare pentru coloana', + DB_ERROR_INVALID_DSN => 'DSN invalid', + DB_ERROR_CONNECT_FAILED => 'conectare esuata', + 0 => 'fara eroare', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'data introduse insuficiente', + DB_ERROR_EXTENSION_NOT_FOUND=> 'extensie negasita', + DB_ERROR_NOSUCHDB => 'nu exista baza de date', + DB_ERROR_ACCESS_VIOLATION => 'permisiuni insuficiente' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-ru.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-ru.inc.php new file mode 100644 index 000000000..67d80f2cc --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-ru.inc.php @@ -0,0 +1,34 @@ + 'ru', + DB_ERROR => 'неизвестная ошибка', + DB_ERROR_ALREADY_EXISTS => 'уже существует', + DB_ERROR_CANNOT_CREATE => 'невозможно создать', + DB_ERROR_CANNOT_DELETE => 'невозможно удалить', + DB_ERROR_CANNOT_DROP => 'невозможно удалить (drop)', + DB_ERROR_CONSTRAINT => 'нарушение условий проверки', + DB_ERROR_DIVZERO => 'деление на 0', + DB_ERROR_INVALID => 'неправильно', + DB_ERROR_INVALID_DATE => 'некорректная дата или время', + DB_ERROR_INVALID_NUMBER => 'некорректное число', + DB_ERROR_MISMATCH => 'ошибка', + DB_ERROR_NODBSELECTED => 'БД не выбрана', + DB_ERROR_NOSUCHFIELD => 'не существует поле', + DB_ERROR_NOSUCHTABLE => 'не существует таблица', + DB_ERROR_NOT_CAPABLE => 'СУБД не в состоянии', + DB_ERROR_NOT_FOUND => 'не найдено', + DB_ERROR_NOT_LOCKED => 'не заблокировано', + DB_ERROR_SYNTAX => 'синтаксическая ошибка', + DB_ERROR_UNSUPPORTED => 'не поддерживается', + DB_ERROR_VALUE_COUNT_ON_ROW => 'счетчик значений в строке', + DB_ERROR_INVALID_DSN => 'неправильная DSN', + DB_ERROR_CONNECT_FAILED => 'соединение неуспешно', + 0 => 'нет ошибки', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'предоставлено недостаточно данных', + DB_ERROR_EXTENSION_NOT_FOUND=> 'расширение не найдено', + DB_ERROR_NOSUCHDB => 'не существует БД', + DB_ERROR_ACCESS_VIOLATION => 'недостаточно прав доступа' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-sv.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-sv.inc.php new file mode 100644 index 000000000..d3be6b0e7 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-sv.inc.php @@ -0,0 +1,32 @@ + 'en', + DB_ERROR => 'Okänt fel', + DB_ERROR_ALREADY_EXISTS => 'finns redan', + DB_ERROR_CANNOT_CREATE => 'kan inte skapa', + DB_ERROR_CANNOT_DELETE => 'kan inte ta bort', + DB_ERROR_CANNOT_DROP => 'kan inte släppa', + DB_ERROR_CONSTRAINT => 'begränsning kränkt', + DB_ERROR_DIVZERO => 'division med noll', + DB_ERROR_INVALID => 'ogiltig', + DB_ERROR_INVALID_DATE => 'ogiltigt datum eller tid', + DB_ERROR_INVALID_NUMBER => 'ogiltigt tal', + DB_ERROR_MISMATCH => 'felaktig matchning', + DB_ERROR_NODBSELECTED => 'ingen databas vald', + DB_ERROR_NOSUCHFIELD => 'inget sådant fält', + DB_ERROR_NOSUCHTABLE => 'ingen sådan tabell', + DB_ERROR_NOT_CAPABLE => 'DB backend klarar det inte', + DB_ERROR_NOT_FOUND => 'finns inte', + DB_ERROR_NOT_LOCKED => 'inte låst', + DB_ERROR_SYNTAX => 'syntaxfel', + DB_ERROR_UNSUPPORTED => 'stöds ej', + DB_ERROR_VALUE_COUNT_ON_ROW => 'värde räknat på rad', + DB_ERROR_INVALID_DSN => 'ogiltig DSN', + DB_ERROR_CONNECT_FAILED => 'anslutning misslyckades', + 0 => 'inget fel', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'otillräckligt med data angivet', + DB_ERROR_EXTENSION_NOT_FOUND=> 'utökning hittades ej', + DB_ERROR_NOSUCHDB => 'ingen sådan databas', + DB_ERROR_ACCESS_VIOLATION => 'otillräckliga rättigheter' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-th.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-th.inc.php new file mode 100644 index 000000000..a0685645d --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-th.inc.php @@ -0,0 +1,32 @@ + +$ADODB_LANG_ARRAY = array ( + 'LANG' => 'th', + DB_ERROR => 'error ไม่รู้สาเหตุ', + DB_ERROR_ALREADY_EXISTS => 'มี�?ล้ว', + DB_ERROR_CANNOT_CREATE => 'สร้างไม่ได้', + DB_ERROR_CANNOT_DELETE => 'ลบไม่ได้', + DB_ERROR_CANNOT_DROP => 'drop ไม่ได้', + DB_ERROR_CONSTRAINT => 'constraint violation', + DB_ERROR_DIVZERO => 'หา�?ด้วยสู�?', + DB_ERROR_INVALID => 'ไม่ valid', + DB_ERROR_INVALID_DATE => 'วันที่ เวลา ไม่ valid', + DB_ERROR_INVALID_NUMBER => 'เลขไม่ valid', + DB_ERROR_MISMATCH => 'mismatch', + DB_ERROR_NODBSELECTED => 'ไม่ได้เลือ�?�?านข้อมูล', + DB_ERROR_NOSUCHFIELD => 'ไม่มีฟีลด์นี้', + DB_ERROR_NOSUCHTABLE => 'ไม่มีตารางนี้', + DB_ERROR_NOT_CAPABLE => 'DB backend not capable', + DB_ERROR_NOT_FOUND => 'ไม่พบ', + DB_ERROR_NOT_LOCKED => 'ไม่ได้ล๊อ�?', + DB_ERROR_SYNTAX => 'ผิด syntax', + DB_ERROR_UNSUPPORTED => 'ไม่ support', + DB_ERROR_VALUE_COUNT_ON_ROW => 'value count on row', + DB_ERROR_INVALID_DSN => 'invalid DSN', + DB_ERROR_CONNECT_FAILED => 'ไม่สามารถ connect', + 0 => 'no error', + DB_ERROR_NEED_MORE_DATA => 'ข้อมูลไม่เพียงพอ', + DB_ERROR_EXTENSION_NOT_FOUND=> 'ไม่พบ extension', + DB_ERROR_NOSUCHDB => 'ไม่มีข้อมูลนี้', + DB_ERROR_ACCESS_VIOLATION => 'permissions ไม่พอ' +); diff --git a/app/vendor/adodb/adodb-php/lang/adodb-uk.inc.php b/app/vendor/adodb/adodb-php/lang/adodb-uk.inc.php new file mode 100644 index 000000000..2ace5bc49 --- /dev/null +++ b/app/vendor/adodb/adodb-php/lang/adodb-uk.inc.php @@ -0,0 +1,34 @@ + 'uk', + DB_ERROR => 'невідома помилка', + DB_ERROR_ALREADY_EXISTS => 'вже існує', + DB_ERROR_CANNOT_CREATE => 'неможливо створити', + DB_ERROR_CANNOT_DELETE => 'неможливо видалити', + DB_ERROR_CANNOT_DROP => 'неможливо знищити (drop)', + DB_ERROR_CONSTRAINT => 'порушення умов перевірки', + DB_ERROR_DIVZERO => 'ділення на 0', + DB_ERROR_INVALID => 'неправильно', + DB_ERROR_INVALID_DATE => 'неправильна дата чи час', + DB_ERROR_INVALID_NUMBER => 'неправильне число', + DB_ERROR_MISMATCH => 'помилка', + DB_ERROR_NODBSELECTED => 'не вибрано БД', + DB_ERROR_NOSUCHFIELD => 'не існує поле', + DB_ERROR_NOSUCHTABLE => 'не існує таблиця', + DB_ERROR_NOT_CAPABLE => 'СУБД не в стані', + DB_ERROR_NOT_FOUND => 'не знайдено', + DB_ERROR_NOT_LOCKED => 'не заблоковано', + DB_ERROR_SYNTAX => 'синтаксична помилка', + DB_ERROR_UNSUPPORTED => 'не підтримується', + DB_ERROR_VALUE_COUNT_ON_ROW => 'рахівник значень в стрічці', + DB_ERROR_INVALID_DSN => 'неправильна DSN', + DB_ERROR_CONNECT_FAILED => 'з\'єднання неуспішне', + 0 => 'все гаразд', // DB_OK + DB_ERROR_NEED_MORE_DATA => 'надано недостатньо даних', + DB_ERROR_EXTENSION_NOT_FOUND=> 'розширення не знайдено', + DB_ERROR_NOSUCHDB => 'не існує БД', + DB_ERROR_ACCESS_VIOLATION => 'недостатньо прав доступа' +); diff --git a/app/vendor/adodb/adodb-php/pear/Auth/Container/ADOdb.php b/app/vendor/adodb/adodb-php/pear/Auth/Container/ADOdb.php new file mode 100644 index 000000000..26c3a23e1 --- /dev/null +++ b/app/vendor/adodb/adodb-php/pear/Auth/Container/ADOdb.php @@ -0,0 +1,406 @@ + + Richard Tango-Lowy +*/ + +require_once 'Auth/Container.php'; +require_once 'adodb.inc.php'; +require_once 'adodb-pear.inc.php'; +require_once 'adodb-errorpear.inc.php'; + +/** + * Storage driver for fetching login data from a database using ADOdb-PHP. + * + * This storage driver can use all databases which are supported + * by the ADBdb DB abstraction layer to fetch login data. + * See http://php.weblogs.com/adodb for information on ADOdb. + * NOTE: The ADOdb directory MUST be in your PHP include_path! + * + * @author Richard Tango-Lowy + * @package Auth + * @version $Revision: 1.3 $ + */ +class Auth_Container_ADOdb extends Auth_Container +{ + + /** + * Additional options for the storage container + * @var array + */ + var $options = array(); + + /** + * DB object + * @var object + */ + var $db = null; + var $dsn = ''; + + /** + * User that is currently selected from the DB. + * @var string + */ + var $activeUser = ''; + + // {{{ Constructor + + /** + * Constructor of the container class + * + * Initate connection to the database via PEAR::ADOdb + * + * @param string Connection data or DB object + * @return object Returns an error object if something went wrong + */ + function __construct($dsn) + { + $this->_setDefaults(); + + if (is_array($dsn)) { + $this->_parseOptions($dsn); + + if (empty($this->options['dsn'])) { + PEAR::raiseError('No connection parameters specified!'); + } + } else { + // Extract db_type from dsn string. + $this->options['dsn'] = $dsn; + } + } + + // }}} + // {{{ _connect() + + /** + * Connect to database by using the given DSN string + * + * @access private + * @param string DSN string + * @return mixed Object on error, otherwise bool + */ + function _connect($dsn) + { + if (is_string($dsn) || is_array($dsn)) { + if(!$this->db) { + $this->db = ADONewConnection($dsn); + if( $err = ADODB_Pear_error() ) { + return PEAR::raiseError($err); + } + } + + } else { + return PEAR::raiseError('The given dsn was not valid in file ' . __FILE__ . ' at line ' . __LINE__, + 41, + PEAR_ERROR_RETURN, + null, + null + ); + } + + if(!$this->db) { + return PEAR::raiseError(ADODB_Pear_error()); + } else { + return true; + } + } + + // }}} + // {{{ _prepare() + + /** + * Prepare database connection + * + * This function checks if we have already opened a connection to + * the database. If that's not the case, a new connection is opened. + * + * @access private + * @return mixed True or a DB error object. + */ + function _prepare() + { + if(!$this->db) { + $res = $this->_connect($this->options['dsn']); + } + return true; + } + + // }}} + // {{{ query() + + /** + * Prepare query to the database + * + * This function checks if we have already opened a connection to + * the database. If that's not the case, a new connection is opened. + * After that the query is passed to the database. + * + * @access public + * @param string Query string + * @return mixed a DB_result object or DB_OK on success, a DB + * or PEAR error on failure + */ + function query($query) + { + $err = $this->_prepare(); + if ($err !== true) { + return $err; + } + return $this->db->query($query); + } + + // }}} + // {{{ _setDefaults() + + /** + * Set some default options + * + * @access private + * @return void + */ + function _setDefaults() + { + $this->options['db_type'] = 'mysql'; + $this->options['table'] = 'auth'; + $this->options['usernamecol'] = 'username'; + $this->options['passwordcol'] = 'password'; + $this->options['dsn'] = ''; + $this->options['db_fields'] = ''; + $this->options['cryptType'] = 'md5'; + } + + // }}} + // {{{ _parseOptions() + + /** + * Parse options passed to the container class + * + * @access private + * @param array + */ + function _parseOptions($array) + { + foreach ($array as $key => $value) { + if (isset($this->options[$key])) { + $this->options[$key] = $value; + } + } + + /* Include additional fields if they exist */ + if(!empty($this->options['db_fields'])){ + if(is_array($this->options['db_fields'])){ + $this->options['db_fields'] = join($this->options['db_fields'], ', '); + } + $this->options['db_fields'] = ', '.$this->options['db_fields']; + } + } + + // }}} + // {{{ fetchData() + + /** + * Get user information from database + * + * This function uses the given username to fetch + * the corresponding login data from the database + * table. If an account that matches the passed username + * and password is found, the function returns true. + * Otherwise it returns false. + * + * @param string Username + * @param string Password + * @return mixed Error object or boolean + */ + function fetchData($username, $password) + { + // Prepare for a database query + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + // Find if db_fields contains a *, i so assume all col are selected + if(strstr($this->options['db_fields'], '*')){ + $sql_from = "*"; + } + else{ + $sql_from = $this->options['usernamecol'] . ", ".$this->options['passwordcol'].$this->options['db_fields']; + } + + $query = "SELECT ".$sql_from. + " FROM ".$this->options['table']. + " WHERE ".$this->options['usernamecol']." = " . $this->db->Quote($username); + + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $rset = $this->db->Execute( $query ); + $res = $rset->fetchRow(); + + if (DB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } + if (!is_array($res)) { + $this->activeUser = ''; + return false; + } + if ($this->verifyPassword(trim($password, "\r\n"), + trim($res[$this->options['passwordcol']], "\r\n"), + $this->options['cryptType'])) { + // Store additional field values in the session + foreach ($res as $key => $value) { + if ($key == $this->options['passwordcol'] || + $key == $this->options['usernamecol']) { + continue; + } + // Use reference to the auth object if exists + // This is because the auth session variable can change so a static call to setAuthData does not make sence + if(is_object($this->_auth_obj)){ + $this->_auth_obj->setAuthData($key, $value); + } else { + Auth::setAuthData($key, $value); + } + } + + return true; + } + + $this->activeUser = $res[$this->options['usernamecol']]; + return false; + } + + // }}} + // {{{ listUsers() + + function listUsers() + { + $err = $this->_prepare(); + if ($err !== true) { + return PEAR::raiseError($err->getMessage(), $err->getCode()); + } + + $retVal = array(); + + // Find if db_fileds contains a *, i so assume all col are selected + if(strstr($this->options['db_fields'], '*')){ + $sql_from = "*"; + } + else{ + $sql_from = $this->options['usernamecol'] . ", ".$this->options['passwordcol'].$this->options['db_fields']; + } + + $query = sprintf("SELECT %s FROM %s", + $sql_from, + $this->options['table'] + ); + $res = $this->db->getAll($query, null, DB_FETCHMODE_ASSOC); + + if (DB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } else { + foreach ($res as $user) { + $user['username'] = $user[$this->options['usernamecol']]; + $retVal[] = $user; + } + } + return $retVal; + } + + // }}} + // {{{ addUser() + + /** + * Add user to the storage container + * + * @access public + * @param string Username + * @param string Password + * @param mixed Additional information that are stored in the DB + * + * @return mixed True on success, otherwise error object + */ + function addUser($username, $password, $additional = "") + { + if (function_exists($this->options['cryptType'])) { + $cryptFunction = $this->options['cryptType']; + } else { + $cryptFunction = 'md5'; + } + + $additional_key = ''; + $additional_value = ''; + + if (is_array($additional)) { + foreach ($additional as $key => $value) { + $additional_key .= ', ' . $key; + $additional_value .= ", '" . $value . "'"; + } + } + + $query = sprintf("INSERT INTO %s (%s, %s%s) VALUES ('%s', '%s'%s)", + $this->options['table'], + $this->options['usernamecol'], + $this->options['passwordcol'], + $additional_key, + $username, + $cryptFunction($password), + $additional_value + ); + + $res = $this->query($query); + + if (DB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } else { + return true; + } + } + + // }}} + // {{{ removeUser() + + /** + * Remove user from the storage container + * + * @access public + * @param string Username + * + * @return mixed True on success, otherwise error object + */ + function removeUser($username) + { + $query = sprintf("DELETE FROM %s WHERE %s = '%s'", + $this->options['table'], + $this->options['usernamecol'], + $username + ); + + $res = $this->query($query); + + if (DB::isError($res)) { + return PEAR::raiseError($res->getMessage(), $res->getCode()); + } else { + return true; + } + } + + // }}} +} + +function showDbg( $string ) { + print " +-- $string

"; +} +function dump( $var, $str, $vardump = false ) { + print "

$str

";
+	( !$vardump ) ? ( print_r( $var )) : ( var_dump( $var ));
+	print "
"; +} diff --git a/app/vendor/adodb/adodb-php/pear/auth_adodb_example.php b/app/vendor/adodb/adodb-php/pear/auth_adodb_example.php new file mode 100644 index 000000000..3b7cf5e85 --- /dev/null +++ b/app/vendor/adodb/adodb-php/pear/auth_adodb_example.php @@ -0,0 +1,25 @@ + +
+ + + +
+ $dsn, 'table' => 'auth', 'cryptType' => 'none', + 'usernamecol' => 'username', 'passwordcol' => 'password'); +$a = new Auth("ADOdb", $params, "loginFunction"); +$a->start(); + +if ($a->getAuth()) { + echo "Success"; + // * The output of your site goes here. +} diff --git a/app/vendor/adodb/adodb-php/pear/readme.Auth.txt b/app/vendor/adodb/adodb-php/pear/readme.Auth.txt new file mode 100644 index 000000000..f5b162cc1 --- /dev/null +++ b/app/vendor/adodb/adodb-php/pear/readme.Auth.txt @@ -0,0 +1,20 @@ +From: Rich Tango-Lowy (richtl#arscognita.com) +Date: Sat, May 29, 2004 11:20 am + +OK, I hacked out an ADOdb container for PEAR-Auth. The error handling's +a bit of a mess, but all the methods work. + +Copy ADOdb.php to your pear/Auth/Container/ directory. + +Use the ADOdb container exactly as you would the DB +container, but specify 'ADOdb' instead of 'DB': + +$dsn = "mysql://myuser:mypass@localhost/authdb"; +$a = new Auth("ADOdb", $dsn, "loginFunction"); + + +------------------- + +John Lim adds: + +See http://pear.php.net/manual/en/package.authentication.php diff --git a/app/vendor/adodb/adodb-php/perf/perf-db2.inc.php b/app/vendor/adodb/adodb-php/perf/perf-db2.inc.php new file mode 100644 index 000000000..143fc3d55 --- /dev/null +++ b/app/vendor/adodb/adodb-php/perf/perf-db2.inc.php @@ -0,0 +1,108 @@ + array('RATIO', + "SELECT + case when sum(POOL_DATA_L_READS+POOL_INDEX_L_READS)=0 then 0 + else 100*(1-sum(POOL_DATA_P_READS+POOL_INDEX_P_READS)/sum(POOL_DATA_L_READS+POOL_INDEX_L_READS)) end + FROM TABLE(SNAPSHOT_APPL('',-2)) as t", + '=WarnCacheRatio'), + + 'Data Cache', + 'data cache buffers' => array('DATAC', + 'select sum(npages) from SYSCAT.BUFFERPOOLS', + 'See tuning reference.' ), + 'cache blocksize' => array('DATAC', + 'select avg(pagesize) from SYSCAT.BUFFERPOOLS', + '' ), + 'data cache size' => array('DATAC', + 'select sum(npages*pagesize) from SYSCAT.BUFFERPOOLS', + '' ), + 'Connections', + 'current connections' => array('SESS', + "SELECT count(*) FROM TABLE(SNAPSHOT_APPL_INFO('',-2)) as t", + ''), + + false + ); + + + function __construct(&$conn) + { + $this->conn = $conn; + } + + function Explain($sql,$partial=false) + { + $save = $this->conn->LogSQL(false); + if ($partial) { + $sqlq = $this->conn->qstr($sql.'%'); + $arr = $this->conn->GetArray("select distinct sql1 from adodb_logsql where sql1 like $sqlq"); + if ($arr) { + foreach($arr as $row) { + $sql = reset($row); + if (crc32($sql) == $partial) break; + } + } + } + $qno = rand(); + $ok = $this->conn->Execute("EXPLAIN PLAN SET QUERYNO=$qno FOR $sql"); + ob_start(); + if (!$ok) echo "

Have EXPLAIN tables been created?

"; + else { + $rs = $this->conn->Execute("select * from explain_statement where queryno=$qno"); + if ($rs) rs2html($rs); + } + $s = ob_get_contents(); + ob_end_clean(); + $this->conn->LogSQL($save); + + $s .= $this->Tracer($sql); + return $s; + } + + /** + * Gets a list of tables + * + * @param int $throwaway discarded variable to match the parent method + * @return string The formatted table list + */ + function Tables($throwaway=0) + { + $rs = $this->conn->Execute("select tabschema,tabname,card as rows, + npages pages_used,fpages pages_allocated, tbspace tablespace + from syscat.tables where tabschema not in ('SYSCAT','SYSIBM','SYSSTAT') order by 1,2"); + return rs2html($rs,false,false,false,false); + } +} diff --git a/app/vendor/adodb/adodb-php/perf/perf-informix.inc.php b/app/vendor/adodb/adodb-php/perf/perf-informix.inc.php new file mode 100644 index 000000000..e5cfb25d9 --- /dev/null +++ b/app/vendor/adodb/adodb-php/perf/perf-informix.inc.php @@ -0,0 +1,71 @@ + array('RATIOH', + "select round((1-(wt.value / (rd.value + wr.value)))*100,2) + from sysmaster:sysprofile wr, sysmaster:sysprofile rd, sysmaster:sysprofile wt + where rd.name = 'pagreads' and + wr.name = 'pagwrites' and + wt.name = 'buffwts'", + '=WarnCacheRatio'), + 'IO', + 'data reads' => array('IO', + "select value from sysmaster:sysprofile where name='pagreads'", + 'Page reads'), + + 'data writes' => array('IO', + "select value from sysmaster:sysprofile where name='pagwrites'", + 'Page writes'), + + 'Connections', + 'current connections' => array('SESS', + 'select count(*) from sysmaster:syssessions', + 'Number of sessions'), + + false + + ); + + function __construct(&$conn) + { + $this->conn = $conn; + } + +} diff --git a/app/vendor/adodb/adodb-php/perf/perf-mssql.inc.php b/app/vendor/adodb/adodb-php/perf/perf-mssql.inc.php new file mode 100644 index 000000000..cd8a6641f --- /dev/null +++ b/app/vendor/adodb/adodb-php/perf/perf-mssql.inc.php @@ -0,0 +1,164 @@ + array('RATIO', + "select round((a.cntr_value*100.0)/b.cntr_value,2) from master.dbo.sysperfinfo a, master.dbo.sysperfinfo b where a.counter_name = 'Buffer cache hit ratio' and b.counter_name='Buffer cache hit ratio base'", + '=WarnCacheRatio'), + 'prepared sql hit ratio' => array('RATIO', + array('dbcc cachestats','Prepared',1,100), + ''), + 'adhoc sql hit ratio' => array('RATIO', + array('dbcc cachestats','Adhoc',1,100), + ''), + 'IO', + 'data reads' => array('IO', + "select cntr_value from master.dbo.sysperfinfo where counter_name = 'Page reads/sec'"), + 'data writes' => array('IO', + "select cntr_value from master.dbo.sysperfinfo where counter_name = 'Page writes/sec'"), + + 'Data Cache', + 'data cache size' => array('DATAC', + "select cntr_value*8192 from master.dbo.sysperfinfo where counter_name = 'Total Pages' and object_name='SQLServer:Buffer Manager'", + '' ), + 'data cache blocksize' => array('DATAC', + "select 8192",'page size'), + 'Connections', + 'current connections' => array('SESS', + '=sp_who', + ''), + 'max connections' => array('SESS', + "SELECT @@MAX_CONNECTIONS", + ''), + + false + ); + + + function __construct(&$conn) + { + if ($conn->dataProvider == 'odbc') { + $this->sql1 = 'sql1'; + //$this->explain = false; + } + $this->conn = $conn; + } + + function Explain($sql,$partial=false) + { + + $save = $this->conn->LogSQL(false); + if ($partial) { + $sqlq = $this->conn->qstr($sql.'%'); + $arr = $this->conn->GetArray("select distinct sql1 from adodb_logsql where sql1 like $sqlq"); + if ($arr) { + foreach($arr as $row) { + $sql = reset($row); + if (crc32($sql) == $partial) break; + } + } + } + + $s = '

Explain: '.htmlspecialchars($sql).'

'; + $this->conn->Execute("SET SHOWPLAN_ALL ON;"); + $sql = str_replace('?',"''",$sql); + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $rs = $this->conn->Execute($sql); + //adodb_printr($rs); + $ADODB_FETCH_MODE = $save; + if ($rs && !$rs->EOF) { + $rs->MoveNext(); + $s .= ''; + while (!$rs->EOF) { + $s .= '\n"; ## NOTE CORRUPT tag is intentional!!!! + $rs->MoveNext(); + } + $s .= '
Rows IO CPU     Plan
'.round($rs->fields[8],1).''.round($rs->fields[9],3).''.round($rs->fields[10],3).'
'.htmlspecialchars($rs->fields[0])."
'; + + $rs->NextRecordSet(); + } + + $this->conn->Execute("SET SHOWPLAN_ALL OFF;"); + $this->conn->LogSQL($save); + $s .= $this->Tracer($sql); + return $s; + } + + function Tables() + { + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + //$this->conn->debug=1; + $s = ''; + $rs1 = $this->conn->Execute("select distinct name from sysobjects where xtype='U'"); + if ($rs1) { + while (!$rs1->EOF) { + $tab = $rs1->fields[0]; + $tabq = $this->conn->qstr($tab); + $rs2 = $this->conn->Execute("sp_spaceused $tabq"); + if ($rs2) { + $s .= ''; + $rs2->Close(); + } + $rs1->MoveNext(); + } + $rs1->Close(); + } + $ADODB_FETCH_MODE = $save; + return $s.'
tablenamesize_in_kindex sizereserved size
'.$tab.''.$rs2->fields[3].''.$rs2->fields[4].''.$rs2->fields[2].'
'; + } + + function sp_who() + { + $arr = $this->conn->GetArray('sp_who'); + return sizeof($arr); + } + + function HealthCheck($cli=false) + { + + $this->conn->Execute('dbcc traceon(3604)'); + $html = adodb_perf::HealthCheck($cli); + $this->conn->Execute('dbcc traceoff(3604)'); + return $html; + } + + +} diff --git a/app/vendor/adodb/adodb-php/perf/perf-mssqlnative.inc.php b/app/vendor/adodb/adodb-php/perf/perf-mssqlnative.inc.php new file mode 100644 index 000000000..b27788821 --- /dev/null +++ b/app/vendor/adodb/adodb-php/perf/perf-mssqlnative.inc.php @@ -0,0 +1,164 @@ + array('RATIO', + "select round((a.cntr_value*100.0)/b.cntr_value,2) from master.dbo.sysperfinfo a, master.dbo.sysperfinfo b where a.counter_name = 'Buffer cache hit ratio' and b.counter_name='Buffer cache hit ratio base'", + '=WarnCacheRatio'), + 'prepared sql hit ratio' => array('RATIO', + array('dbcc cachestats','Prepared',1,100), + ''), + 'adhoc sql hit ratio' => array('RATIO', + array('dbcc cachestats','Adhoc',1,100), + ''), + 'IO', + 'data reads' => array('IO', + "select cntr_value from master.dbo.sysperfinfo where counter_name = 'Page reads/sec'"), + 'data writes' => array('IO', + "select cntr_value from master.dbo.sysperfinfo where counter_name = 'Page writes/sec'"), + + 'Data Cache', + 'data cache size' => array('DATAC', + "select cntr_value*8192 from master.dbo.sysperfinfo where counter_name = 'Total Pages' and object_name='SQLServer:Buffer Manager'", + '' ), + 'data cache blocksize' => array('DATAC', + "select 8192",'page size'), + 'Connections', + 'current connections' => array('SESS', + '=sp_who', + ''), + 'max connections' => array('SESS', + "SELECT @@MAX_CONNECTIONS", + ''), + + false + ); + + + function __construct(&$conn) + { + if ($conn->dataProvider == 'odbc') { + $this->sql1 = 'sql1'; + //$this->explain = false; + } + $this->conn =& $conn; + } + + function Explain($sql,$partial=false) + { + + $save = $this->conn->LogSQL(false); + if ($partial) { + $sqlq = $this->conn->qstr($sql.'%'); + $arr = $this->conn->GetArray("select distinct sql1 from adodb_logsql where sql1 like $sqlq"); + if ($arr) { + foreach($arr as $row) { + $sql = reset($row); + if (crc32($sql) == $partial) break; + } + } + } + + $s = '

Explain: '.htmlspecialchars($sql).'

'; + $this->conn->Execute("SET SHOWPLAN_ALL ON;"); + $sql = str_replace('?',"''",$sql); + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $rs =& $this->conn->Execute($sql); + //adodb_printr($rs); + $ADODB_FETCH_MODE = $save; + if ($rs) { + $rs->MoveNext(); + $s .= ''; + while (!$rs->EOF) { + $s .= '\n"; ## NOTE CORRUPT tag is intentional!!!! + $rs->MoveNext(); + } + $s .= '
Rows IO CPU     Plan
'.round($rs->fields[8],1).''.round($rs->fields[9],3).''.round($rs->fields[10],3).'
'.htmlspecialchars($rs->fields[0])."
'; + + $rs->NextRecordSet(); + } + + $this->conn->Execute("SET SHOWPLAN_ALL OFF;"); + $this->conn->LogSQL($save); + $s .= $this->Tracer($sql); + return $s; + } + + function Tables($orderby='1') + { + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + //$this->conn->debug=1; + $s = ''; + $rs1 = $this->conn->Execute("select distinct name from sysobjects where xtype='U'"); + if ($rs1) { + while (!$rs1->EOF) { + $tab = $rs1->fields[0]; + $tabq = $this->conn->qstr($tab); + $rs2 = $this->conn->Execute("sp_spaceused $tabq"); + if ($rs2) { + $s .= ''; + $rs2->Close(); + } + $rs1->MoveNext(); + } + $rs1->Close(); + } + $ADODB_FETCH_MODE = $save; + return $s.'
tablenamesize_in_kindex sizereserved size
'.$tab.''.$rs2->fields[3].''.$rs2->fields[4].''.$rs2->fields[2].'
'; + } + + function sp_who() + { + $arr = $this->conn->GetArray('sp_who'); + return sizeof($arr); + } + + function HealthCheck($cli=false) + { + + $this->conn->Execute('dbcc traceon(3604)'); + $html = adodb_perf::HealthCheck($cli); + $this->conn->Execute('dbcc traceoff(3604)'); + return $html; + } + + +} diff --git a/app/vendor/adodb/adodb-php/perf/perf-mysql.inc.php b/app/vendor/adodb/adodb-php/perf/perf-mysql.inc.php new file mode 100644 index 000000000..adccbdc92 --- /dev/null +++ b/app/vendor/adodb/adodb-php/perf/perf-mysql.inc.php @@ -0,0 +1,316 @@ + array('RATIO', + '=GetKeyHitRatio', + '=WarnCacheRatio'), + 'InnoDB cache hit ratio' => array('RATIO', + '=GetInnoDBHitRatio', + '=WarnCacheRatio'), + 'data cache hit ratio' => array('HIDE', # only if called + '=FindDBHitRatio', + '=WarnCacheRatio'), + 'sql cache hit ratio' => array('RATIO', + '=GetQHitRatio', + ''), + 'IO', + 'data reads' => array('IO', + '=GetReads', + 'Number of selects (Key_reads is not accurate)'), + 'data writes' => array('IO', + '=GetWrites', + 'Number of inserts/updates/deletes * coef (Key_writes is not accurate)'), + + 'Data Cache', + 'MyISAM data cache size' => array('DATAC', + array("show variables", 'key_buffer_size'), + '' ), + 'BDB data cache size' => array('DATAC', + array("show variables", 'bdb_cache_size'), + '' ), + 'InnoDB data cache size' => array('DATAC', + array("show variables", 'innodb_buffer_pool_size'), + '' ), + 'Memory Usage', + 'read buffer size' => array('CACHE', + array("show variables", 'read_buffer_size'), + '(per session)'), + 'sort buffer size' => array('CACHE', + array("show variables", 'sort_buffer_size'), + 'Size of sort buffer (per session)' ), + 'table cache' => array('CACHE', + array("show variables", 'table_cache'), + 'Number of tables to keep open'), + 'Connections', + 'current connections' => array('SESS', + array('show status','Threads_connected'), + ''), + 'max connections' => array( 'SESS', + array("show variables",'max_connections'), + ''), + + false + ); + + function __construct(&$conn) + { + $this->conn = $conn; + } + + function Explain($sql,$partial=false) + { + + if (strtoupper(substr(trim($sql),0,6)) !== 'SELECT') return '

Unable to EXPLAIN non-select statement

'; + $save = $this->conn->LogSQL(false); + if ($partial) { + $sqlq = $this->conn->qstr($sql.'%'); + $arr = $this->conn->GetArray("select distinct sql1 from adodb_logsql where sql1 like $sqlq"); + if ($arr) { + foreach($arr as $row) { + $sql = reset($row); + if (crc32($sql) == $partial) break; + } + } + } + $sql = str_replace('?',"''",$sql); + + if ($partial) { + $sqlq = $this->conn->qstr($sql.'%'); + $sql = $this->conn->GetOne("select sql1 from adodb_logsql where sql1 like $sqlq"); + } + + $s = '

Explain: '.htmlspecialchars($sql).'

'; + $rs = $this->conn->Execute('EXPLAIN '.$sql); + $s .= rs2html($rs,false,false,false,false); + $this->conn->LogSQL($save); + $s .= $this->Tracer($sql); + return $s; + } + + function Tables() + { + if (!$this->tablesSQL) return false; + + $rs = $this->conn->Execute($this->tablesSQL); + if (!$rs) return false; + + $html = rs2html($rs,false,false,false,false); + return $html; + } + + function GetReads() + { + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->conn->fetchMode !== false) $savem = $this->conn->SetFetchMode(false); + + $rs = $this->conn->Execute('show status'); + + if (isset($savem)) $this->conn->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + if (!$rs) return 0; + $val = 0; + while (!$rs->EOF) { + switch($rs->fields[0]) { + case 'Com_select': + $val = $rs->fields[1]; + $rs->Close(); + return $val; + } + $rs->MoveNext(); + } + + $rs->Close(); + + return $val; + } + + function GetWrites() + { + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->conn->fetchMode !== false) $savem = $this->conn->SetFetchMode(false); + + $rs = $this->conn->Execute('show status'); + + if (isset($savem)) $this->conn->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + if (!$rs) return 0; + $val = 0.0; + while (!$rs->EOF) { + switch($rs->fields[0]) { + case 'Com_insert': + $val += $rs->fields[1]; break; + case 'Com_delete': + $val += $rs->fields[1]; break; + case 'Com_update': + $val += $rs->fields[1]/2; + $rs->Close(); + return $val; + } + $rs->MoveNext(); + } + + $rs->Close(); + + return $val; + } + + function FindDBHitRatio() + { + // first find out type of table + //$this->conn->debug=1; + + global $ADODB_FETCH_MODE; + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->conn->fetchMode !== false) $savem = $this->conn->SetFetchMode(false); + + $rs = $this->conn->Execute('show table status'); + + if (isset($savem)) $this->conn->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + if (!$rs) return ''; + $type = strtoupper($rs->fields[1]); + $rs->Close(); + switch($type){ + case 'MYISAM': + case 'ISAM': + return $this->DBParameter('MyISAM cache hit ratio').' (MyISAM)'; + case 'INNODB': + return $this->DBParameter('InnoDB cache hit ratio').' (InnoDB)'; + default: + return $type.' not supported'; + } + + } + + function GetQHitRatio() + { + //Total number of queries = Qcache_inserts + Qcache_hits + Qcache_not_cached + $hits = $this->_DBParameter(array("show status","Qcache_hits")); + $total = $this->_DBParameter(array("show status","Qcache_inserts")); + $total += $this->_DBParameter(array("show status","Qcache_not_cached")); + + $total += $hits; + if ($total) return round(($hits*100)/$total,2); + return 0; + } + + /* + Use session variable to store Hit percentage, because MySQL + does not remember last value of SHOW INNODB STATUS hit ratio + + # 1st query to SHOW INNODB STATUS + 0.00 reads/s, 0.00 creates/s, 0.00 writes/s + Buffer pool hit rate 1000 / 1000 + + # 2nd query to SHOW INNODB STATUS + 0.00 reads/s, 0.00 creates/s, 0.00 writes/s + No buffer pool activity since the last printout + */ + function GetInnoDBHitRatio() + { + global $ADODB_FETCH_MODE; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($this->conn->fetchMode !== false) $savem = $this->conn->SetFetchMode(false); + + $rs = $this->conn->Execute('show engine innodb status'); + + if (isset($savem)) $this->conn->SetFetchMode($savem); + $ADODB_FETCH_MODE = $save; + + if (!$rs || $rs->EOF) return 0; + $stat = $rs->fields[0]; + $rs->Close(); + $at = strpos($stat,'Buffer pool hit rate'); + $stat = substr($stat,$at,200); + if (preg_match('!Buffer pool hit rate\s*([0-9]*) / ([0-9]*)!',$stat,$arr)) { + $val = 100*$arr[1]/$arr[2]; + $_SESSION['INNODB_HIT_PCT'] = $val; + return round($val,2); + } else { + if (isset($_SESSION['INNODB_HIT_PCT'])) return $_SESSION['INNODB_HIT_PCT']; + return 0; + } + return 0; + } + + function GetKeyHitRatio() + { + $hits = $this->_DBParameter(array("show status","Key_read_requests")); + $reqs = $this->_DBParameter(array("show status","Key_reads")); + if ($reqs == 0) return 0; + + return round(($hits/($reqs+$hits))*100,2); + } + + // start hack + var $optimizeTableLow = 'CHECK TABLE %s FAST QUICK'; + var $optimizeTableHigh = 'OPTIMIZE TABLE %s'; + + /** + * @see adodb_perf#optimizeTable + */ + function optimizeTable( $table, $mode = ADODB_OPT_LOW) + { + if ( !is_string( $table)) return false; + + $conn = $this->conn; + if ( !$conn) return false; + + $sql = ''; + switch( $mode) { + case ADODB_OPT_LOW : $sql = $this->optimizeTableLow; break; + case ADODB_OPT_HIGH : $sql = $this->optimizeTableHigh; break; + default : + { + // May dont use __FUNCTION__ constant for BC (__FUNCTION__ Added in PHP 4.3.0) + ADOConnection::outp( sprintf( "

%s: '%s' using of undefined mode '%s'

", __CLASS__, __FUNCTION__, $mode)); + return false; + } + } + $sql = sprintf( $sql, $table); + + return $conn->Execute( $sql) !== false; + } + // end hack +} diff --git a/app/vendor/adodb/adodb-php/perf/perf-oci8.inc.php b/app/vendor/adodb/adodb-php/perf/perf-oci8.inc.php new file mode 100644 index 000000000..69df104fa --- /dev/null +++ b/app/vendor/adodb/adodb-php/perf/perf-oci8.inc.php @@ -0,0 +1,703 @@ + array('RATIOH', + "select round((1-(phy.value / (cur.value + con.value)))*100,2) + from v\$sysstat cur, v\$sysstat con, v\$sysstat phy + where cur.name = 'db block gets' and + con.name = 'consistent gets' and + phy.name = 'physical reads'", + '=WarnCacheRatio'), + + 'sql cache hit ratio' => array( 'RATIOH', + 'select round(100*(sum(pins)-sum(reloads))/sum(pins),2) from v$librarycache', + 'increase shared_pool_size if too ratio low'), + + 'datadict cache hit ratio' => array('RATIOH', + "select + round((1 - (sum(getmisses) / (sum(gets) + + sum(getmisses))))*100,2) + from v\$rowcache", + 'increase shared_pool_size if too ratio low'), + + 'memory sort ratio' => array('RATIOH', + "SELECT ROUND((100 * b.VALUE) /DECODE ((a.VALUE + b.VALUE), + 0,1,(a.VALUE + b.VALUE)),2) +FROM v\$sysstat a, + v\$sysstat b +WHERE a.name = 'sorts (disk)' +AND b.name = 'sorts (memory)'", + "% of memory sorts compared to disk sorts - should be over 95%"), + + 'IO', + 'data reads' => array('IO', + "select value from v\$sysstat where name='physical reads'"), + + 'data writes' => array('IO', + "select value from v\$sysstat where name='physical writes'"), + + 'Data Cache', + + 'data cache buffers' => array( 'DATAC', + "select a.value/b.value from v\$parameter a, v\$parameter b + where a.name = 'db_cache_size' and b.name= 'db_block_size'", + 'Number of cache buffers. Tune db_cache_size if the data cache hit ratio is too low.'), + 'data cache blocksize' => array('DATAC', + "select value from v\$parameter where name='db_block_size'", + '' ), + + 'Memory Pools', + 'Mem Max Target (11g+)' => array( 'DATAC', + "select value from v\$parameter where name = 'memory_max_target'", + 'The memory_max_size is the maximum value to which memory_target can be set.' ), + 'Memory target (11g+)' => array( 'DATAC', + "select value from v\$parameter where name = 'memory_target'", + 'If memory_target is defined then SGA and PGA targets are consolidated into one memory_target.' ), + 'SGA Max Size' => array( 'DATAC', + "select nvl(value,0)/1024.0/1024 || 'M' from v\$parameter where name = 'sga_max_size'", + 'The sga_max_size is the maximum value to which sga_target can be set.' ), + 'SGA target' => array( 'DATAC', + "select nvl(value,0)/1024.0/1024 || 'M' from v\$parameter where name = 'sga_target'", + 'If sga_target is defined then data cache, shared, java and large pool size can be 0. This is because all these pools are consolidated into one sga_target.' ), + 'PGA aggr target' => array( 'DATAC', + "select nvl(value,0)/1024.0/1024 || 'M' from v\$parameter where name = 'pga_aggregate_target'", + 'If pga_aggregate_target is defined then this is the maximum memory that can be allocated for cursor operations such as sorts, group by, joins, merges. When in doubt, set it to 20% of sga_target.' ), + 'data cache size' => array('DATAC', + "select value from v\$parameter where name = 'db_cache_size'", + 'db_cache_size' ), + 'shared pool size' => array('DATAC', + "select value from v\$parameter where name = 'shared_pool_size'", + 'shared_pool_size, which holds shared sql, stored procedures, dict cache and similar shared structs' ), + 'java pool size' => array('DATAJ', + "select value from v\$parameter where name = 'java_pool_size'", + 'java_pool_size' ), + 'large pool buffer size' => array('CACHE', + "select value from v\$parameter where name='large_pool_size'", + 'this pool is for large mem allocations (not because it is larger than shared pool), for MTS sessions, parallel queries, io buffers (large_pool_size) ' ), + + 'dynamic memory usage' => array('CACHE', "select '-' from dual", '=DynMemoryUsage'), + + 'Connections', + 'current connections' => array('SESS', + 'select count(*) from sys.v_$session where username is not null', + ''), + 'max connections' => array( 'SESS', + "select value from v\$parameter where name='sessions'", + ''), + + 'Memory Utilization', + 'data cache utilization ratio' => array('RATIOU', + "select round((1-bytes/sgasize)*100, 2) + from (select sum(bytes) sgasize from sys.v_\$sgastat) s, sys.v_\$sgastat f + where name = 'free memory' and pool = 'shared pool'", + 'Percentage of data cache actually in use - should be over 85%'), + + 'shared pool utilization ratio' => array('RATIOU', + 'select round((sga.bytes/case when p.value=0 then sga.bytes else to_number(p.value) end)*100,2) + from v$sgastat sga, v$parameter p + where sga.name = \'free memory\' and sga.pool = \'shared pool\' + and p.name = \'shared_pool_size\'', + 'Percentage of shared pool actually used - too low is bad, too high is worse'), + + 'large pool utilization ratio' => array('RATIOU', + "select round((1-bytes/sgasize)*100, 2) + from (select sum(bytes) sgasize from sys.v_\$sgastat) s, sys.v_\$sgastat f + where name = 'free memory' and pool = 'large pool'", + 'Percentage of large_pool actually in use - too low is bad, too high is worse'), + 'sort buffer size' => array('CACHE', + "select value from v\$parameter where name='sort_area_size'", + 'max in-mem sort_area_size (per query), uses memory in pga' ), + + /*'pga usage at peak' => array('RATIOU', + '=PGA','Mb utilization at peak transactions (requires Oracle 9i+)'),*/ + 'Transactions', + 'rollback segments' => array('ROLLBACK', + "select count(*) from sys.v_\$rollstat", + ''), + + 'peak transactions' => array('ROLLBACK', + "select max_utilization tx_hwm + from sys.v_\$resource_limit + where resource_name = 'transactions'", + 'Taken from high-water-mark'), + 'max transactions' => array('ROLLBACK', + "select value from v\$parameter where name = 'transactions'", + 'max transactions / rollback segments < 3.5 (or transactions_per_rollback_segment)'), + 'Parameters', + 'cursor sharing' => array('CURSOR', + "select value from v\$parameter where name = 'cursor_sharing'", + 'Cursor reuse strategy. Recommended is FORCE (8i+) or SIMILAR (9i+). See cursor_sharing.'), + /* + 'cursor reuse' => array('CURSOR', + "select count(*) from (select sql_text_wo_constants, count(*) + from t1 + group by sql_text_wo_constants +having count(*) > 100)",'These are sql statements that should be using bind variables'),*/ + 'index cache cost' => array('COST', + "select value from v\$parameter where name = 'optimizer_index_caching'", + '=WarnIndexCost'), + 'random page cost' => array('COST', + "select value from v\$parameter where name = 'optimizer_index_cost_adj'", + '=WarnPageCost'), + 'Waits', + 'Recent wait events' => array('WAITS','select \'Top 5 events\' from dual','=TopRecentWaits'), +// 'Historical wait SQL' => array('WAITS','select \'Last 2 days\' from dual','=TopHistoricalWaits'), -- requires AWR license + 'Backup', + 'Achivelog Mode' => array('BACKUP', 'select log_mode from v$database', '=LogMode'), + + 'DBID' => array('BACKUP','select dbid from v$database','Primary key of database, used for recovery with an RMAN Recovery Catalog'), + 'Archive Log Dest' => array('BACKUP', "SELECT NVL(v1.value,v2.value) +FROM v\$parameter v1, v\$parameter v2 WHERE v1.name='log_archive_dest' AND v2.name='log_archive_dest_10'", ''), + + 'Flashback Area' => array('BACKUP', "select nvl(value,'Flashback Area not used') from v\$parameter where name=lower('DB_RECOVERY_FILE_DEST')", 'Flashback area is a folder where all backup data and logs can be stored and managed by Oracle. If Error: message displayed, then it is not in use.'), + + 'Flashback Usage' => array('BACKUP', "select nvl('-','Flashback Area not used') from v\$parameter where name=lower('DB_RECOVERY_FILE_DEST')", '=FlashUsage', 'Flashback area usage.'), + + 'Control File Keep Time' => array('BACKUP', "select value from v\$parameter where name='control_file_record_keep_time'",'No of days to keep RMAN info in control file. Recommended set to x2 or x3 times the frequency of your full backup.'), + 'Recent RMAN Jobs' => array('BACKUP', "select '-' from dual", "=RMAN"), + + // 'Control File Keep Time' => array('BACKUP', "select value from v\$parameter where name='control_file_record_keep_time'",'No of days to keep RMAN info in control file. I recommend it be set to x2 or x3 times the frequency of your full backup.'), + 'Storage', 'Tablespaces' => array('TABLESPACE', "select '-' from dual", "=TableSpace"), + false + + ); + + + function __construct(&$conn) + { + global $gSQLBlockRows; + + $gSQLBlockRows = 1000; + $savelog = $conn->LogSQL(false); + $this->version = $conn->ServerInfo(); + $conn->LogSQL($savelog); + $this->conn = $conn; + } + + function LogMode() + { + $mode = $this->conn->GetOne("select log_mode from v\$database"); + + if ($mode == 'ARCHIVELOG') return 'To turn off archivelog:
+

+        SQLPLUS> connect sys as sysdba;
+        SQLPLUS> shutdown immediate;
+
+        SQLPLUS> startup mount exclusive;
+        SQLPLUS> alter database noarchivelog;
+        SQLPLUS> alter database open;
+
'; + + return 'To turn on archivelog:
+

+        SQLPLUS> connect sys as sysdba;
+        SQLPLUS> shutdown immediate;
+
+        SQLPLUS> startup mount exclusive;
+        SQLPLUS> alter database archivelog;
+        SQLPLUS> archive log start;
+        SQLPLUS> alter database open;
+
'; + } + + function TopRecentWaits() + { + + $rs = $this->conn->Execute("select * from ( + select event, round(100*time_waited/(select sum(time_waited) from v\$system_event where wait_class <> 'Idle'),1) \"% Wait\", + total_waits,time_waited, average_wait,wait_class from v\$system_event where wait_class <> 'Idle' order by 2 desc + ) where rownum <=5"); + + $ret = rs2html($rs,false,false,false,false); + return " 

".$ret." 

"; + + } + + function TopHistoricalWaits() + { + $days = 2; + + $rs = $this->conn->Execute("select * from ( SELECT + b.wait_class,B.NAME, + round(sum(wait_time+TIME_WAITED)/1000000) waitsecs, + parsing_schema_name, + C.SQL_TEXT, a.sql_id +FROM V\$ACTIVE_SESSION_HISTORY A + join V\$EVENT_NAME B on A.EVENT# = B.EVENT# + join V\$SQLAREA C on A.SQL_ID = C.SQL_ID +WHERE A.SAMPLE_TIME BETWEEN sysdate-$days and sysdate + and parsing_schema_name not in ('SYS','SYSMAN','DBSNMP','SYSTEM') +GROUP BY b.wait_class,parsing_schema_name,C.SQL_TEXT, B.NAME,A.sql_id +order by 3 desc) where rownum <=10"); + + $ret = rs2html($rs,false,false,false,false); + return " 

".$ret." 

"; + + } + + function TableSpace() + { + + $rs = $this->conn->Execute( + "select tablespace_name,round(sum(bytes)/1024/1024) as Used_MB,round(sum(maxbytes)/1024/1024) as Max_MB, round(sum(bytes)/sum(maxbytes),4) * 100 as PCT + from dba_data_files + group by tablespace_name order by 2 desc"); + + $ret = "

Tablespace".rs2html($rs,false,false,false,false); + + $rs = $this->conn->Execute("select * from dba_data_files order by tablespace_name, 1"); + $ret .= "

Datafile".rs2html($rs,false,false,false,false); + + return " 

".$ret." 

"; + } + + function RMAN() + { + $rs = $this->conn->Execute("select * from (select start_time, end_time, operation, status, mbytes_processed, output_device_type + from V\$RMAN_STATUS order by start_time desc) where rownum <=10"); + + $ret = rs2html($rs,false,false,false,false); + return " 

".$ret." 

"; + + } + + function DynMemoryUsage() + { + if (@$this->version['version'] >= 11) { + $rs = $this->conn->Execute("select component, current_size/1024./1024 as \"CurrSize (M)\" from V\$MEMORY_DYNAMIC_COMPONENTS"); + + } else + $rs = $this->conn->Execute("select name, round(bytes/1024./1024,2) as \"CurrSize (M)\" from V\$sgainfo"); + + + $ret = rs2html($rs,false,false,false,false); + return " 

".$ret." 

"; + } + + function FlashUsage() + { + $rs = $this->conn->Execute("select * from V\$FLASH_RECOVERY_AREA_USAGE"); + $ret = rs2html($rs,false,false,false,false); + return " 

".$ret." 

"; + } + + function WarnPageCost($val) + { + if ($val == 100 && $this->version['version'] < 10) $s = 'Too High. '; + else $s = ''; + + return $s.'Recommended is 20-50 for TP, and 50 for data warehouses. Default is 100. See optimizer_index_cost_adj. '; + } + + function WarnIndexCost($val) + { + if ($val == 0 && $this->version['version'] < 10) $s = 'Too Low. '; + else $s = ''; + + return $s.'Percentage of indexed data blocks expected in the cache. + Recommended is 20 (fast disk array) to 30 (slower hard disks). Default is 0. + See optimizer_index_caching.'; + } + + function PGA() + { + + //if ($this->version['version'] < 9) return 'Oracle 9i or later required'; + } + + function PGA_Advice() + { + $t = "

PGA Advice Estimate

"; + if ($this->version['version'] < 9) return $t.'Oracle 9i or later required'; + + $rs = $this->conn->Execute('select a.MB, + case when a.targ = 1 then \'<<= Current \' + when a.targ < 1 or a.pct <= b.pct then null + else + \'- BETTER than Current by \'||round(a.pct/b.pct*100-100,2)||\'%\' end as "Percent Improved", + a.targ as "PGA Size Factor",a.pct "% Perf" + from + (select round(pga_target_for_estimate/1024.0/1024.0,0) MB, + pga_target_factor targ,estd_pga_cache_hit_percentage pct,rownum as r + from v$pga_target_advice) a left join + (select round(pga_target_for_estimate/1024.0/1024.0,0) MB, + pga_target_factor targ,estd_pga_cache_hit_percentage pct,rownum as r + from v$pga_target_advice) b on + a.r = b.r+1 where + b.pct < 100'); + if (!$rs) return $t."Only in 9i or later"; + // $rs->Close(); + if ($rs->EOF) return $t."PGA could be too big"; + + return $t.rs2html($rs,false,false,true,false); + } + + function Explain($sql,$partial=false) + { + $savelog = $this->conn->LogSQL(false); + $rs = $this->conn->SelectLimit("select ID FROM PLAN_TABLE"); + if (!$rs) { + echo "

Missing PLAN_TABLE

+
+CREATE TABLE PLAN_TABLE (
+  STATEMENT_ID                    VARCHAR2(30),
+  TIMESTAMP                       DATE,
+  REMARKS                         VARCHAR2(80),
+  OPERATION                       VARCHAR2(30),
+  OPTIONS                         VARCHAR2(30),
+  OBJECT_NODE                     VARCHAR2(128),
+  OBJECT_OWNER                    VARCHAR2(30),
+  OBJECT_NAME                     VARCHAR2(30),
+  OBJECT_INSTANCE                 NUMBER(38),
+  OBJECT_TYPE                     VARCHAR2(30),
+  OPTIMIZER                       VARCHAR2(255),
+  SEARCH_COLUMNS                  NUMBER,
+  ID                              NUMBER(38),
+  PARENT_ID                       NUMBER(38),
+  POSITION                        NUMBER(38),
+  COST                            NUMBER(38),
+  CARDINALITY                     NUMBER(38),
+  BYTES                           NUMBER(38),
+  OTHER_TAG                       VARCHAR2(255),
+  PARTITION_START                 VARCHAR2(255),
+  PARTITION_STOP                  VARCHAR2(255),
+  PARTITION_ID                    NUMBER(38),
+  OTHER                           LONG,
+  DISTRIBUTION                    VARCHAR2(30)
+);
+
"; + return false; + } + + $rs->Close(); + // $this->conn->debug=1; + + if ($partial) { + $sqlq = $this->conn->qstr($sql.'%'); + $arr = $this->conn->GetArray("select distinct sql1 from adodb_logsql where sql1 like $sqlq"); + if ($arr) { + foreach($arr as $row) { + $sql = reset($row); + if (crc32($sql) == $partial) break; + } + } + } + + $s = "

Explain: ".htmlspecialchars($sql)."

"; + + $this->conn->BeginTrans(); + $id = "ADODB ".microtime(); + + $rs = $this->conn->Execute("EXPLAIN PLAN SET STATEMENT_ID='$id' FOR $sql"); + $m = $this->conn->ErrorMsg(); + if ($m) { + $this->conn->RollbackTrans(); + $this->conn->LogSQL($savelog); + $s .= "

$m

"; + return $s; + } + $rs = $this->conn->Execute(" + select + '
'||lpad('--', (level-1)*2,'-') || trim(operation) || ' ' || trim(options)||'
' as Operation, + object_name,COST,CARDINALITY,bytes + FROM plan_table +START WITH id = 0 and STATEMENT_ID='$id' +CONNECT BY prior id=parent_id and statement_id='$id'"); + + $s .= rs2html($rs,false,false,false,false); + $this->conn->RollbackTrans(); + $this->conn->LogSQL($savelog); + $s .= $this->Tracer($sql,$partial); + return $s; + } + + function CheckMemory() + { + if ($this->version['version'] < 9) return 'Oracle 9i or later required'; + + $rs = $this->conn->Execute(" +select a.name Buffer_Pool, b.size_for_estimate as cache_mb_estimate, + case when b.size_factor=1 then + '<<= Current' + when a.estd_physical_read_factor-b.estd_physical_read_factor > 0.001 and b.estd_physical_read_factor<1 then + '- BETTER than current by ' || round((1-b.estd_physical_read_factor)/b.estd_physical_read_factor*100,2) || '%' + else ' ' end as RATING, + b.estd_physical_read_factor \"Phys. Reads Factor\", + round((a.estd_physical_read_factor-b.estd_physical_read_factor)/b.estd_physical_read_factor*100,2) as \"% Improve\" + from (select size_for_estimate,size_factor,estd_physical_read_factor,rownum r,name from v\$db_cache_advice order by name,1) a , + (select size_for_estimate,size_factor,estd_physical_read_factor,rownum r,name from v\$db_cache_advice order by name,1) b + where a.r = b.r-1 and a.name = b.name + "); + if (!$rs) return false; + + /* + The v$db_cache_advice utility show the marginal changes in physical data block reads for different sizes of db_cache_size + */ + $s = "

Data Cache Advice Estimate

"; + if ($rs->EOF) { + $s .= "

Cache that is 50% of current size is still too big

"; + } else { + $s .= "Ideal size of Data Cache is when %BETTER gets close to zero."; + $s .= rs2html($rs,false,false,false,false); + } + return $s.$this->PGA_Advice(); + } + + /* + Generate html for suspicious/expensive sql + */ + function tohtml(&$rs,$type) + { + $o1 = $rs->FetchField(0); + $o2 = $rs->FetchField(1); + $o3 = $rs->FetchField(2); + if ($rs->EOF) return '

None found

'; + $check = ''; + $sql = ''; + $s = "\n\n'; + while (!$rs->EOF) { + if ($check != $rs->fields[0].'::'.$rs->fields[1]) { + if ($check) { + $carr = explode('::',$check); + $prefix = "'; + $suffix = ''; + if (strlen($prefix)>2000) { + $prefix = ''; + $suffix = ''; + } + + $s .= "\n'; + } + $sql = $rs->fields[2]; + $check = $rs->fields[0].'::'.$rs->fields[1]; + } else + $sql .= $rs->fields[2]; + if (substr($sql,strlen($sql)-1) == "\0") $sql = substr($sql,0,strlen($sql)-1); + $rs->MoveNext(); + } + $rs->Close(); + + $carr = explode('::',$check); + $prefix = "'; + $suffix = ''; + if (strlen($prefix)>2000) { + $prefix = ''; + $suffix = ''; + } + $s .= "\n'; + + return $s."
".$o1->name.''.$o2->name.''.$o3->name.'
".$carr[0].''.$carr[1].''.$prefix.$sql.$suffix.'
".$carr[0].''.$carr[1].''.$prefix.$sql.$suffix.'
\n\n"; + } + + // code thanks to Ixora. + // http://www.ixora.com.au/scripts/query_opt.htm + // requires oracle 8.1.7 or later + function SuspiciousSQL($numsql=10) + { + $sql = " +select + substr(to_char(s.pct, '99.00'), 2) || '%' load, + s.executions executes, + p.sql_text +from + ( + select + address, + buffer_gets, + executions, + pct, + rank() over (order by buffer_gets desc) ranking + from + ( + select + address, + buffer_gets, + executions, + 100 * ratio_to_report(buffer_gets) over () pct + from + sys.v_\$sql + where + command_type != 47 and module != 'T.O.A.D.' + ) + where + buffer_gets > 50 * executions + ) s, + sys.v_\$sqltext p +where + s.ranking <= $numsql and + p.address = s.address +order by + 1 desc, s.address, p.piece"; + + global $ADODB_CACHE_MODE; + if (isset($_GET['expsixora']) && isset($_GET['sql'])) { + $partial = empty($_GET['part']); + echo "".$this->Explain($_GET['sql'],$partial)."\n"; + } + + if (isset($_GET['sql'])) return $this->_SuspiciousSQL($numsql); + + $s = ''; + $timer = time(); + $s .= $this->_SuspiciousSQL($numsql); + $timer = time() - $timer; + + if ($timer > $this->noShowIxora) return $s; + $s .= '

'; + + $save = $ADODB_CACHE_MODE; + $ADODB_CACHE_MODE = ADODB_FETCH_NUM; + if ($this->conn->fetchMode !== false) $savem = $this->conn->SetFetchMode(false); + + $savelog = $this->conn->LogSQL(false); + $rs = $this->conn->SelectLimit($sql); + $this->conn->LogSQL($savelog); + + if (isset($savem)) $this->conn->SetFetchMode($savem); + $ADODB_CACHE_MODE = $save; + if ($rs) { + $s .= "\n

Ixora Suspicious SQL

"; + $s .= $this->tohtml($rs,'expsixora'); + } + + return $s; + } + + // code thanks to Ixora. + // http://www.ixora.com.au/scripts/query_opt.htm + // requires oracle 8.1.7 or later + function ExpensiveSQL($numsql = 10) + { + $sql = " +select + substr(to_char(s.pct, '99.00'), 2) || '%' load, + s.executions executes, + p.sql_text +from + ( + select + address, + disk_reads, + executions, + pct, + rank() over (order by disk_reads desc) ranking + from + ( + select + address, + disk_reads, + executions, + 100 * ratio_to_report(disk_reads) over () pct + from + sys.v_\$sql + where + command_type != 47 and module != 'T.O.A.D.' + ) + where + disk_reads > 50 * executions + ) s, + sys.v_\$sqltext p +where + s.ranking <= $numsql and + p.address = s.address +order by + 1 desc, s.address, p.piece +"; + global $ADODB_CACHE_MODE; + if (isset($_GET['expeixora']) && isset($_GET['sql'])) { + $partial = empty($_GET['part']); + echo "".$this->Explain($_GET['sql'],$partial)."\n"; + } + if (isset($_GET['sql'])) { + $var = $this->_ExpensiveSQL($numsql); + return $var; + } + + $s = ''; + $timer = time(); + $s .= $this->_ExpensiveSQL($numsql); + $timer = time() - $timer; + if ($timer > $this->noShowIxora) return $s; + + $s .= '

'; + $save = $ADODB_CACHE_MODE; + $ADODB_CACHE_MODE = ADODB_FETCH_NUM; + if ($this->conn->fetchMode !== false) $savem = $this->conn->SetFetchMode(false); + + $savelog = $this->conn->LogSQL(false); + $rs = $this->conn->Execute($sql); + $this->conn->LogSQL($savelog); + + if (isset($savem)) $this->conn->SetFetchMode($savem); + $ADODB_CACHE_MODE = $save; + + if ($rs) { + $s .= "\n

Ixora Expensive SQL

"; + $s .= $this->tohtml($rs,'expeixora'); + } + + return $s; + } + + function clearsql() + { + $perf_table = adodb_perf::table(); + // using the naive "delete from $perf_table where created<".$this->conn->sysTimeStamp will cause the table to lock, possibly + // for a long time + $sql = +"DECLARE cnt pls_integer; +BEGIN + cnt := 0; + FOR rec IN (SELECT ROWID AS rr FROM $perf_table WHERE createdconn->Execute($sql); + } + +} diff --git a/app/vendor/adodb/adodb-php/perf/perf-postgres.inc.php b/app/vendor/adodb/adodb-php/perf/perf-postgres.inc.php new file mode 100644 index 000000000..bd0ccb1b5 --- /dev/null +++ b/app/vendor/adodb/adodb-php/perf/perf-postgres.inc.php @@ -0,0 +1,154 @@ + array('RATIO', + "select case when count(*)=3 then 'TRUE' else 'FALSE' end from pg_settings where (name='stats_block_level' or name='stats_row_level' or name='stats_start_collector') and setting='on' ", + 'Value must be TRUE to enable hit ratio statistics (stats_start_collector,stats_row_level and stats_block_level must be set to true in postgresql.conf)'), + 'data cache hit ratio' => array('RATIO', + "select case when blks_hit=0 then 0 else round( ((1-blks_read::float/blks_hit)*100)::numeric, 2) end from pg_stat_database where datname='\$DATABASE'", + '=WarnCacheRatio'), + 'IO', + 'data reads' => array('IO', + 'select sum(heap_blks_read+toast_blks_read) from pg_statio_user_tables', + ), + 'data writes' => array('IO', + 'select round((sum(n_tup_ins/4.0+n_tup_upd/8.0+n_tup_del/4.0)/16)::numeric,2) from pg_stat_user_tables', + 'Count of inserts/updates/deletes * coef'), + + 'Data Cache', + 'data cache buffers' => array('DATAC', + "select setting from pg_settings where name='shared_buffers'", + 'Number of cache buffers. Tuning'), + 'cache blocksize' => array('DATAC', + 'select 8192', + '(estimate)' ), + 'data cache size' => array( 'DATAC', + "select setting::integer*8192 from pg_settings where name='shared_buffers'", + '' ), + 'operating system cache size' => array( 'DATA', + "select setting::integer*8192 from pg_settings where name='effective_cache_size'", + '(effective cache size)' ), + 'Memory Usage', + # Postgres 7.5 changelog: Rename server parameters SortMem and VacuumMem to work_mem and maintenance_work_mem; + 'sort/work buffer size' => array('CACHE', + "select setting::integer*1024 from pg_settings where name='sort_mem' or name = 'work_mem' order by name", + 'Size of sort buffer (per query)' ), + 'Connections', + 'current connections' => array('SESS', + 'select count(*) from pg_stat_activity', + ''), + 'max connections' => array('SESS', + "select setting from pg_settings where name='max_connections'", + ''), + 'Parameters', + 'rollback buffers' => array('COST', + "select setting from pg_settings where name='wal_buffers'", + 'WAL buffers'), + 'random page cost' => array('COST', + "select setting from pg_settings where name='random_page_cost'", + 'Cost of doing a seek (default=4). See random_page_cost'), + false + ); + + function __construct(&$conn) + { + $this->conn = $conn; + } + + var $optimizeTableLow = 'VACUUM %s'; + var $optimizeTableHigh = 'VACUUM ANALYZE %s'; + +/** + * @see adodb_perf#optimizeTable + */ + + function optimizeTable($table, $mode = ADODB_OPT_LOW) + { + if(! is_string($table)) return false; + + $conn = $this->conn; + if (! $conn) return false; + + $sql = ''; + switch($mode) { + case ADODB_OPT_LOW : $sql = $this->optimizeTableLow; break; + case ADODB_OPT_HIGH: $sql = $this->optimizeTableHigh; break; + default : + { + ADOConnection::outp(sprintf("

%s: '%s' using of undefined mode '%s'

", __CLASS__, 'optimizeTable', $mode)); + return false; + } + } + $sql = sprintf($sql, $table); + + return $conn->Execute($sql) !== false; + } + + function Explain($sql,$partial=false) + { + $save = $this->conn->LogSQL(false); + + if ($partial) { + $sqlq = $this->conn->qstr($sql.'%'); + $arr = $this->conn->GetArray("select distinct distinct sql1 from adodb_logsql where sql1 like $sqlq"); + if ($arr) { + foreach($arr as $row) { + $sql = reset($row); + if (crc32($sql) == $partial) break; + } + } + } + $sql = str_replace('?',"''",$sql); + $s = '

Explain: '.htmlspecialchars($sql).'

'; + $rs = $this->conn->Execute('EXPLAIN '.$sql); + $this->conn->LogSQL($save); + $s .= '
';
+		if ($rs)
+			while (!$rs->EOF) {
+				$s .= reset($rs->fields)."\n";
+				$rs->MoveNext();
+			}
+		$s .= '
'; + $s .= $this->Tracer($sql,$partial); + return $s; + } +} diff --git a/app/vendor/adodb/adodb-php/pivottable.inc.php b/app/vendor/adodb/adodb-php/pivottable.inc.php new file mode 100644 index 000000000..dd7d5d88a --- /dev/null +++ b/app/vendor/adodb/adodb-php/pivottable.inc.php @@ -0,0 +1,188 @@ +databaseType,'access') !== false; + // note - vfp 6 still doesn' work even with IIF enabled || $db->databaseType == 'vfp'; + + //$hidecnt = false; + + if ($where) $where = "\nWHERE $where"; + if (!is_array($colfield)) $colarr = $db->GetCol("select distinct $colfield from $tables $where order by 1"); + if (!$aggfield) $hidecnt = false; + + $sel = "$rowfields, "; + if (is_array($colfield)) { + foreach ($colfield as $k => $v) { + $k = trim($k); + if (!$hidecnt) { + $sel .= $iif ? + "\n\t$aggfn(IIF($v,1,0)) AS \"$k\", " + : + "\n\t$aggfn(CASE WHEN $v THEN 1 ELSE 0 END) AS \"$k\", "; + } + if ($aggfield) { + $sel .= $iif ? + "\n\t$aggfn(IIF($v,$aggfield,0)) AS \"$sumlabel$k\", " + : + "\n\t$aggfn(CASE WHEN $v THEN $aggfield ELSE 0 END) AS \"$sumlabel$k\", "; + } + } + } else { + foreach ($colarr as $v) { + if (!is_numeric($v)) $vq = $db->qstr($v); + else $vq = $v; + $v = trim($v); + if (strlen($v) == 0 ) $v = 'null'; + if (!$hidecnt) { + $sel .= $iif ? + "\n\t$aggfn(IIF($colfield=$vq,1,0)) AS \"$v\", " + : + "\n\t$aggfn(CASE WHEN $colfield=$vq THEN 1 ELSE 0 END) AS \"$v\", "; + } + if ($aggfield) { + if ($hidecnt) $label = $v; + else $label = "{$v}_$aggfield"; + $sel .= $iif ? + "\n\t$aggfn(IIF($colfield=$vq,$aggfield,0)) AS \"$label\", " + : + "\n\t$aggfn(CASE WHEN $colfield=$vq THEN $aggfield ELSE 0 END) AS \"$label\", "; + } + } + } + if ($aggfield && $aggfield != '1'){ + $agg = "$aggfn($aggfield)"; + $sel .= "\n\t$agg as \"$sumlabel$aggfield\", "; + } + + if ($showcount) + $sel .= "\n\tSUM(1) as Total"; + else + $sel = substr($sel,0,strlen($sel)-2); + + + // Strip aliases + $rowfields = preg_replace('/ AS (\w+)/i', '', $rowfields); + + $sql = "SELECT $sel \nFROM $tables $where \nGROUP BY $rowfields"; + + return $sql; + } + +/* EXAMPLES USING MS NORTHWIND DATABASE */ +if (0) { + +# example1 +# +# Query the main "product" table +# Set the rows to CompanyName and QuantityPerUnit +# and the columns to the Categories +# and define the joins to link to lookup tables +# "categories" and "suppliers" +# + + $sql = PivotTableSQL( + $gDB, # adodb connection + 'products p ,categories c ,suppliers s', # tables + 'CompanyName,QuantityPerUnit', # row fields + 'CategoryName', # column fields + 'p.CategoryID = c.CategoryID and s.SupplierID= p.SupplierID' # joins/where +); + print "
$sql";
+ $rs = $gDB->Execute($sql);
+ rs2html($rs);
+
+/*
+Generated SQL:
+
+SELECT CompanyName,QuantityPerUnit,
+	SUM(CASE WHEN CategoryName='Beverages' THEN 1 ELSE 0 END) AS "Beverages",
+	SUM(CASE WHEN CategoryName='Condiments' THEN 1 ELSE 0 END) AS "Condiments",
+	SUM(CASE WHEN CategoryName='Confections' THEN 1 ELSE 0 END) AS "Confections",
+	SUM(CASE WHEN CategoryName='Dairy Products' THEN 1 ELSE 0 END) AS "Dairy Products",
+	SUM(CASE WHEN CategoryName='Grains/Cereals' THEN 1 ELSE 0 END) AS "Grains/Cereals",
+	SUM(CASE WHEN CategoryName='Meat/Poultry' THEN 1 ELSE 0 END) AS "Meat/Poultry",
+	SUM(CASE WHEN CategoryName='Produce' THEN 1 ELSE 0 END) AS "Produce",
+	SUM(CASE WHEN CategoryName='Seafood' THEN 1 ELSE 0 END) AS "Seafood",
+	SUM(1) as Total
+FROM products p ,categories c ,suppliers s  WHERE p.CategoryID = c.CategoryID and s.SupplierID= p.SupplierID
+GROUP BY CompanyName,QuantityPerUnit
+*/
+//=====================================================================
+
+# example2
+#
+# Query the main "product" table
+# Set the rows to CompanyName and QuantityPerUnit
+# and the columns to the UnitsInStock for diiferent ranges
+# and define the joins to link to lookup tables
+# "categories" and "suppliers"
+#
+ $sql = PivotTableSQL(
+ 	$gDB,										# adodb connection
+ 	'products p ,categories c ,suppliers s',	# tables
+	'CompanyName,QuantityPerUnit',				# row fields
+												# column ranges
+array(
+' 0 ' => 'UnitsInStock <= 0',
+"1 to 5" => '0 < UnitsInStock and UnitsInStock <= 5',
+"6 to 10" => '5 < UnitsInStock and UnitsInStock <= 10',
+"11 to 15"  => '10 < UnitsInStock and UnitsInStock <= 15',
+"16+" =>'15 < UnitsInStock'
+),
+	' p.CategoryID = c.CategoryID and s.SupplierID= p.SupplierID', # joins/where
+	'UnitsInStock', 							# sum this field
+	'Sum'										# sum label prefix
+);
+ print "
$sql";
+ $rs = $gDB->Execute($sql);
+ rs2html($rs);
+ /*
+ Generated SQL:
+
+SELECT CompanyName,QuantityPerUnit,
+	SUM(CASE WHEN UnitsInStock <= 0 THEN UnitsInStock ELSE 0 END) AS "Sum  0 ",
+	SUM(CASE WHEN 0 < UnitsInStock and UnitsInStock <= 5 THEN UnitsInStock ELSE 0 END) AS "Sum 1 to 5",
+	SUM(CASE WHEN 5 < UnitsInStock and UnitsInStock <= 10 THEN UnitsInStock ELSE 0 END) AS "Sum 6 to 10",
+	SUM(CASE WHEN 10 < UnitsInStock and UnitsInStock <= 15 THEN UnitsInStock ELSE 0 END) AS "Sum 11 to 15",
+	SUM(CASE WHEN 15 < UnitsInStock THEN UnitsInStock ELSE 0 END) AS "Sum 16+",
+	SUM(UnitsInStock) AS "Sum UnitsInStock",
+	SUM(1) as Total
+FROM products p ,categories c ,suppliers s  WHERE  p.CategoryID = c.CategoryID and s.SupplierID= p.SupplierID
+GROUP BY CompanyName,QuantityPerUnit
+ */
+}
diff --git a/app/vendor/adodb/adodb-php/replicate/adodb-replicate.inc.php b/app/vendor/adodb/adodb-php/replicate/adodb-replicate.inc.php
new file mode 100644
index 000000000..9aaf3c429
--- /dev/null
+++ b/app/vendor/adodb/adodb-php/replicate/adodb-replicate.inc.php
@@ -0,0 +1,1181 @@
+compat. If compat set to 1.0, then $lastUpdateFld not used during MergeData.
+
+1.0		Apr 2009
+Added support for MFFA
+
+0.9 	? 2008
+First release
+
+
+	Note: this code assumes that comments such as  / *    * / ar`e allowed which works with:
+	Note: this code assumes that comments such as  / *    * / are allowed which works with:
+		 mssql, postgresql, oracle, mssql
+
+	Replication engine to
+	 	- copy table structures and data from different databases (e.g. mysql to oracle)
+		  for replication purposes
+		- generate CREATE TABLE, CREATE INDEX, INSERT ... for installation scripts
+
+	Table Structure copying includes
+		- fields and limited subset of types
+		- optional default values
+		- indexes
+		- but not constraints
+
+
+	Two modes of data copy:
+
+	ReplicateData
+		- Copy from src to dest, with update of status of copy back to src,
+		  with configurable src SELECT where clause
+
+	MergeData
+		- Copy from src to dest based on last mod date field and/or copied flag field
+
+	Default settings are
+		- do not execute, generate sql ($rep->execute = false)
+		- do not delete records in dest table first ($rep->deleteFirst = false).
+			if $rep->deleteFirst is true and primary keys are defined,
+			then no deletion will occur unless *INSERTONLY* is defined in pkey array
+		- only commit once at the end of every ReplicateData ($rep->commitReplicate = true)
+		- do not autocommit every x records processed ($rep->commitRecs = -1)
+		- even if error occurs on one record, continue copying remaining records ($rep->neverAbort = true)
+		- debugging turned off ($rep->debug = false)
+*/
+
+class ADODB_Replicate {
+	var $connSrc;
+	var $connDest;
+
+	var $connSrc2 = false;
+	var $connDest2 = false;
+	var $ddSrc;
+	var $ddDest;
+
+	var $execute = false;
+	var $debug = false;
+	var $deleteFirst = false;
+	var $commitReplicate = true; // commit at end of replicatedata
+	var $commitRecs = -1; // only commit at end of ReplicateData()
+
+	var $selFilter = false;
+	var $fieldFilter = false;
+	var $indexFilter = false;
+	var $updateFilter = false;
+	var $insertFilter = false;
+	var $updateSrcFn = false;
+
+	var $limitRecs = false;
+
+	var $neverAbort = true;
+	var $copyTableDefaults = false; // turn off because functions defined as defaults will not work when copied
+	var $errHandler = false; // name of error handler function, if used.
+	var $htmlSpecialChars = true; 	// if execute false, then output with htmlspecialchars enabled.
+									// Will autoconfigure itself. No need to modify
+
+	var $trgSuffix = '_mrgTr';
+	var $idxSuffix = '_mrgidx';
+	var $trLogic = '1 = 1';
+	var $datesAreTimeStamps = false;
+
+	var $oracleSequence = false;
+	var $readUncommitted = false;  // read without obeying shared locks for fast select (mssql)
+
+	var $compat = false;
+	// connSrc2 and connDest2 are only required if the db driver
+	// does not allow updates back to src db in first connection (the select connection),
+	// so we need 2nd connection
+	function __construct($connSrc, $connDest, $connSrc2=false, $connDest2=false)
+	{
+
+		if (strpos($connSrc->databaseType,'odbtp') !== false) {
+			$connSrc->_bindInputArray = false;  # bug in odbtp, binding fails
+		}
+
+		if (strpos($connDest->databaseType,'odbtp') !== false) {
+			$connDest->_bindInputArray = false;  # bug in odbtp, binding fails
+		}
+
+		$this->connSrc = $connSrc;
+		$this->connDest = $connDest;
+
+		$this->connSrc2 = ($connSrc2) ? $connSrc2 : $connSrc;
+		$this->connDest2 = ($connDest2) ? $connDest2 : $connDest;
+
+		$this->ddSrc = NewDataDictionary($connSrc);
+		$this->ddDest = NewDataDictionary($connDest);
+		$this->htmlSpecialChars = isset($_SERVER['HTTP_HOST']);
+	}
+
+	function ExecSQL($sql)
+	{
+		if (!is_array($sql)) $sql[] = $sql;
+
+		$ret = true;
+		foreach($sql as $s)
+			if (!$this->execute) echo "
",$s.";\n
"; + else { + $ok = $this->connDest->Execute($s); + if (!$ok) + if ($this->neverAbort) $ret = false; + else return false; + } + + return $ret; + } + + /* + We assume replication between $table and $desttable only works if the field names and types match for both tables. + + Also $table and desttable can have different names. + */ + + function CopyTableStruct($table,$desttable='') + { + $sql = $this->CopyTableStructSQL($table,$desttable); + if (empty($sql)) return false; + return $this->ExecSQL($sql); + } + + function RunFieldFilter(&$fld, $mode = '') + { + if ($this->fieldFilter) { + $fn = $this->fieldFilter; + return $fn($fld, $mode); + } else + return $fld; + } + + function RunUpdateFilter($table, $fld, $val) + { + if ($this->updateFilter) { + $fn = $this->updateFilter; + return $fn($table, $fld, $val); + } else + return $val; + } + + function RunInsertFilter($table, $fld, &$val) + { + if ($this->insertFilter) { + $fn = $this->insertFilter; + return $fn($table, $fld, $val); + } else + return $fld; + } + + /* + $mode = INS or UPD + + The lastUpdateFld holds the field that counts the number of updates or the date of last mod. This ensures that + if the rec was modified after replicatedata retrieves the data but before we update back the src record, + we don't set the copiedflag='Y' yet. + */ + function RunUpdateSrcFn($srcdb, $table, $fldoffsets, $row, $where, $mode, $dest_insertid=null, $lastUpdateFld='') + { + if (!$this->updateSrcFn) return; + + $bindarr = array(); + foreach($fldoffsets as $k) { + $bindarr[$k] = $row[$k]; + } + $last = sizeof($row); + + if ($lastUpdateFld && $row[$last-1]) { + $ds = $row[$last-1]; + if (strpos($ds,':') !== false) $s = $srcdb->DBTimeStamp($ds); + else $s = $srcdb->qstr($ds); + $where = "WHERE $lastUpdateFld = $s and $where"; + } else + $where = "WHERE $where"; + $fn = $this->updateSrcFn; + if (is_array($fn)) { + if (sizeof($fn) == 1) $set = reset($fn); + else $set = @$fn[$mode]; + if ($set) { + + if (strlen($dest_insertid) == 0) $dest_insertid = 'null'; + $set = str_replace('$INSERT_ID',$dest_insertid,$set); + + $sql = "UPDATE $table SET $set $where "; + $ok = $srcdb->Execute($sql,$bindarr); + if (!$ok) { + echo $srcdb->ErrorMsg(),"
\n"; + die(); + } + } + } else $fn($srcdb, $table, $row, $where, $bindarr, $mode, $dest_insertid); + + } + + function CopyTableStructSQL($table, $desttable='',$dropdest =false) + { + if (!$desttable) { + $desttable = $table; + $prefixidx = ''; + } else + $prefixidx = $desttable; + + $conn = $this->connSrc; + $types = $conn->MetaColumns($table); + if (!$types) { + echo "$table does not exist in source db
\n"; + return array(); + } + if (!$dropdest && $this->connDest->MetaColumns($desttable)) { + echo "$desttable already exists in dest db
\n"; + return array(); + } + if ($this->debug) var_dump($types); + $sa = array(); + $idxcols = array(); + + foreach($types as $name => $t) { + $s = ''; + $mt = $this->ddSrc->MetaType($t->type); + $len = $t->max_length; + $fldname = $this->RunFieldFilter($t->name,'TABLE'); + if (!$fldname) continue; + + $s .= $fldname . ' '.$mt; + if (isset($t->scale)) $precision = '.'.$t->scale; + else $precision = ''; + if ($mt == 'C' or $mt == 'X') $s .= "($len)"; + else if ($mt == 'N' && $precision) $s .= "($len$precision)"; + + if ($mt == 'R') $idxcols[] = $fldname; + + if ($this->copyTableDefaults) { + if (isset($t->default_value)) { + $v = $t->default_value; + if ($mt == 'C' or $mt == 'X') $v = $this->connDest->qstr($v); // might not work as this could be function + $s .= ' DEFAULT '.$v; + } + } + + $sa[] = $s; + } + + $s = implode(",\n",$sa); + + // dump adodb intermediate data dictionary format + if ($this->debug) echo '
'.$s.'
'; + + $sqla = $this->ddDest->CreateTableSQL($desttable,$s); + + /* + if ($idxcols) { + $idxoptions = array('UNIQUE'=>1); + $sqla2 = $this->ddDest->_IndexSQL($table.'_'.$fldname.'_SERIAL', $desttable, $idxcols,$idxoptions); + $sqla = array_merge($sqla,$sqla2); + }*/ + + $idxs = $conn->MetaIndexes($table); + if ($idxs) + foreach($idxs as $name => $iarr) { + $idxoptions = array(); + $fldnames = array(); + + if(!empty($iarr['unique'])) { + $idxoptions['UNIQUE'] = 1; + } + + foreach($iarr['columns'] as $fld) { + $fldnames[] = $this->RunFieldFilter($fld,'TABLE'); + } + + $idxname = $prefixidx.str_replace($table,$desttable,$name); + + if (!empty($this->indexFilter)) { + $fn = $this->indexFilter; + $idxname = $fn($desttable,$idxname,$fldnames,$idxoptions); + } + $sqla2 = $this->ddDest->_IndexSQL($idxname, $desttable, $fldnames,$idxoptions); + $sqla = array_merge($sqla,$sqla2); + } + + return $sqla; + } + + function _clearcache() + { + + } + + function _concat($v) + { + return $this->connDest->concat("' ","chr(".ord($v).")","'"); + } + + function fixupbinary($v) + { + return str_replace( + array("\r","\n"), + array($this->_concat("\r"),$this->_concat("\n")), + $v ); + } + + function SwapDBs() + { + $o = $this->connSrc; + $this->connSrc = $this->connDest; + $this->connDest = $o; + + + $o = $this->connSrc2; + $this->connSrc2 = $this->connDest2; + $this->connDest2 = $o; + + $o = $this->ddSrc; + $this->ddSrc = $this->ddDest; + $this->ddDest = $o; + } + + /* + // if no uniqflds defined, then all desttable recs will be deleted before insert + // $where clause must include the WHERE word if used + // if $this->commitRecs is set to a +ve value, then it will autocommit every $this->commitRecs records + // -- this should never be done with 7x24 db's + + Returns an array: + $arr[0] = true if no error, false if error + $arr[1] = number of recs processed + $arr[2] = number of successful inserts + $arr[3] = number of successful updates + + ReplicateData() params: + + $table = src table name + $desttable = dest table name, leave blank to use src table name + $uniqflds = array() = an array. If set, then inserts and updates will occur. eg. array('PK1', 'PK2'); + To prevent updates to desttable (allow only to src table), add '*INSERTONLY*' or '*ONLYINSERT*' to array. + + Sometimes you are replicating a src table with an autoinc primary key. + You sometimes create recs in the dest table. The dest table has to retrieve the + src table's autoinc key (stored in a 2nd field) so you can match the two tables. + + To define this, and the uniqflds contains nested arrays. Copying from autoinc table to other table: + array(array($destpkey), array($destfld_holds_src_autoinc_pkey)) + + Copying from normal table to autoinc table: + array(array($destpkey), array(), array($srcfld_holds_dest_autoinc_pkey)) + + $where = where clause for SELECT from $table $where. Include the WHERE reserved word in beginning. + You can put ORDER BY at the end also + $ignoreflds = array(), list of fields to ignore. e.g. array('FLD1',FLD2'); + $dstCopyDateFld = date field on $desttable to update with current date + $extraflds allows you to add additional flds to insert/update. Format + array(fldname => $fldval) + $fldval itself can be an array or a string. If an array, then + $extraflds = array($fldname => array($insertval, $updateval)) + + Thus we have the following behaviours: + + a. Delete all data in $desttable then insert from src $table + + $rep->execute = true; + $rep->ReplicateData($table, $desttable) + + b. Update $desttable if record exists (based on $uniqflds), otherwise insert. + + $rep->execute = true; + $rep->ReplicateData($table, $desttable, $array($pkey1, $pkey2)) + + c. Select from src $table all data modified since a date. Then update $desttable + if record exists (based on $uniqflds), otherwise insert + + $rep->execute = true; + $rep->ReplicateData($table, $desttable, array($pkey1, $pkey2), "WHERE update_datetime_fld > $LAST_REFRESH") + + d. Insert all records into $desttable modified after a certain id (or time) in src $table: + + $rep->execute = true; + $rep->ReplicateData($table, $desttable, false, "WHERE id_fld > $LAST_ID_SAVED", true); + + + For (a) to (d), returns array: array($boolean_ok_fail, $no_recs_selected_from_src_db, $no_recs_inserted, $no_recs_updated); + + e. Generate sample SQL: + + $rep->execute = false; + $rep->ReplicateData(....); + + This returns $array, which contains: + + $array['SEL'] = select stmt from src db + $array['UPD'] = update stmt to dest db + $array['INS'] = insert stmt to dest db + + + Error-handling + ============== + Default is never abort if error occurs. You can set $rep->neverAbort = false; to force replication to abort if an error occurs. + + + Value Filtering + ======== + Sometimes you might need to modify/massage the data before the code works. Assume that the value used for True and False is + 'T' and 'F' in src DB, but is 'Y' and 'N' in dest DB for field[2] in select stmt. You can do this by + + $rep->filterSelect = 'filter'; + $rep->ReplicateData(...); + + function filter($table,& $fields, $deleteFirst) + { + if ($table == 'SOMETABLE') { + if ($fields[2] == 'T') $fields[2] = 'Y'; + else if ($fields[2] == 'F') $fields[2] = 'N'; + } + } + + We pass in $deleteFirst as that determines the order of the fields (which are numeric-based): + TRUE: the order of fields matches the src table order + FALSE: the order of fields is all non-primary key fields first, followed by primary key fields. This is because it needs + to match the UPDATE statement, which is UPDATE $table SET f2 = ?, f3 = ? ... WHERE f1 = ? + + Name Filtering + ========= + Sometimes field names that are legal in one RDBMS can be illegal in another. + We allow you to handle this using a field filter. + Also if you don't want to replicate certain fields, just return false. + + $rep->fieldFilter = 'ffilter'; + + function ffilter(&$fld,$mode) + { + $uf = strtoupper($fld); + switch($uf) { + case 'GROUP': + if ($mode == 'SELECT') $fld = '"Group"'; + return 'GroupFld'; + + case 'PRIVATEFLD': # do not replicate + return false; + } + return $fld; + } + + + UPDATE FILTERING + ================ + Sometimes, when we want to update + UPDATE table SET fld = val WHERE .... + + we want to modify val. To do so, define + + $rep->updateFilter = 'ufilter'; + + function ufilter($table, $fld, $val) + { + return "nvl($fld, $val)"; + } + + + Sending back audit info back to src Table + ========================================= + + Use $rep->updateSrcFn. This can be an array of strings, or the name of a php function to call. + + If an array of strings is defined, then it will perform an update statement... + + UPDATE srctable SET $string WHERE .... + + With $string set to the array you define. If a new record was inserted into desttable, then the + 'INS' string is used ($INSERT_ID will be replaced with the real INSERT_ID, if any), + and if an update then use the 'UPD' string. + + array( + 'INS' => 'insertid = $INSERT_ID, copieddate=getdate(), copied = 1', + 'UPD' => 'copieddate=getdate(), copied = 1' + ) + + If a single string array is defined, then it will be used for both insert and update. + array('copieddate=getdate(), copied = 1') + + Note that the where clause is automatically defined by the system. + + If $rep->updateSrcFn is a PHP function name, then it will be called with the following params: + + $fn($srcConnection, $tableName, $row, $where, $bindarr, $mode, $dest_insertid) + + $srcConnection - source db connection + $tableName - source tablename + $row - array holding records updated into dest + $where - where clause to be used (uses bind vars) + $bindarr - array holding bind variables for where clause + $mode - INS or UPD + $dest_insertid - when mode=INS, then the insert_id is stored here. + + + oracle mssql + ---> insert + mssqlid <--- insert_id + ----> update with mssqlid + <---- update with mssqlid + + + TODO: add src pkey and dest pkey for updates. Also sql stmt needs to be tuned, so dest pkey, src pkey + */ + + + function ReplicateData($table, $desttable = '', $uniqflds = array(), $where = '',$ignore_flds = array(), + $dstCopyDateFld='', $extraflds = array(), $lastUpdateFld = '') + { + if (is_array($where)) { + $wheresrc = $where[0]; + $wheredest = $where[1]; + } else { + $wheresrc = $wheredest = $where; + } + $dstCopyDateName = $dstCopyDateFld; + $dstCopyDateFld = strtoupper($dstCopyDateFld); + + $this->_clearcache(); + if (is_string($uniqflds) && strlen($uniqflds)) $uniqflds = array($uniqflds); + if (!$desttable) $desttable = $table; + + $uniq = array(); + if ($uniqflds) { + if (is_array(reset($uniqflds))) { + /* + primary key of src and dest tables differ. This means when we perform the select stmts + we retrieve both keys. Then any insert statement will have to ignore one array element. + Any update statement will need to use a different where clause + */ + $destuniqflds = $uniqflds[0]; + if (sizeof($uniqflds)>1 && $uniqflds[1]) // srckey field name in dest table + $srcuniqflds = $uniqflds[1]; + else + $srcuniqflds = array(); + + if (sizeof($uniqflds)>2) + $srcPKDest = reset($uniqflds[2]); + + } else { + $destuniqflds = $uniqflds; + $srcuniqflds = array(); + } + $onlyInsert = false; + foreach($destuniqflds as $k => $u) { + if ($u == '*INSERTONLY*' || $u == '*ONLYINSERT*') { + $onlyInsert = true; + continue; + } + $uniq[strtoupper($u)] = $k; + } + $deleteFirst = $this->deleteFirst; + } else { + $deleteFirst = true; + } + + if ($deleteFirst) $onlyInsert = true; + + if ($ignore_flds) { + foreach($ignore_flds as $u) { + $ignoreflds[strtoupper($u)] = 1; + } + } else + $ignoreflds = array(); + + $src = $this->connSrc; + $dest = $this->connDest; + $src2 = $this->connSrc2; + + $dest->noNullStrings = false; + $src->noNullStrings = false; + $src2->noNullStrings = false; + + if ($src === $dest) $this->execute = false; + + $types = $src->MetaColumns($table); + if (!$types) { + echo "Source $table does not exist
\n"; + return array(); + } + $dtypes = $dest->MetaColumns($desttable); + if (!$dtypes) { + echo "Destination $desttable does not exist
\n"; + return array(); + } + $sa = array(); + $selflds = array(); + $wheref = array(); + $wheres = array(); + $srcwheref = array(); + $fldoffsets = array(); + $k = 0; + foreach($types as $name => $t) { + $name2 = strtoupper($this->RunFieldFilter($name,'SELECT')); + // handle quotes + if ($name2 && $name2[0] == '"' && $name2[strlen($name2)-1] == '"') $name22 = substr($name2,1,strlen($name2)-2); + elseif ($name2 && $name2[0] == '`' && $name2[strlen($name2)-1] == '`') $name22 = substr($name2,1,strlen($name2)-2); + else $name22 = $name2; + + //else $name22 = $name2; // this causes problem for quotes strip above + + if (!isset($dtypes[($name22)]) || !$name2) { + if ($this->debug) echo " Skipping $name ==> $name2 as not in destination $desttable
"; + continue; + } + + if ($name2 == $dstCopyDateFld) { + $dstCopyDateName = $t->name; + continue; + } + + $fld = $t->name; + $fldval = $t->name; + $mt = $src->MetaType($t->type); + if ($this->datesAreTimeStamps && $mt == 'D') $mt = 'T'; + if ($mt == 'D') $fldval = $dest->DBDate($fldval); + elseif ($mt == 'T') $fldval = $dest->DBTimeStamp($fldval); + $ufld = strtoupper($fld); + + if (isset($ignoreflds[($name2)]) && !isset($uniq[$ufld])) { + continue; + } + + if ($this->debug) echo " field=$fld type=$mt fldval=$fldval
"; + + if (!isset($uniq[$ufld])) { + + $selfld = $fld; + $fld = $this->RunFieldFilter($selfld,'SELECT'); + $selflds[] = $selfld; + + $p = $dest->Param($k); + + if ($mt == 'D') $p = $dest->DBDate($p, true); + else if ($mt == 'T') $p = $dest->DBTimeStamp($p, true); + + # UPDATES + $sets[] = "$fld = ".$this->RunUpdateFilter($desttable, $fld, $p); + + # INSERTS + $insflds[] = $this->RunInsertFilter($desttable,$fld, $p); $params[] = $p; + $k++; + } else { + $fld = $this->RunFieldFilter($fld); + $wheref[] = $fld; + if (!empty($srcuniqflds)) $srcwheref[] = $srcuniqflds[$uniq[$ufld]]; + if ($mt == 'C') { # normally we don't include the primary key in the insert if it is numeric, but ok if varchar + $insertpkey = true; + } + } + } + + + foreach($extraflds as $fld => $evals) { + if (!is_array($evals)) $evals = array($evals, $evals); + $insflds[] = $this->RunInsertFilter($desttable,$fld, $p); $params[] = $evals[0]; + $sets[] = "$fld = ".$evals[1]; + } + + if ($dstCopyDateFld) { + $sets[] = "$dstCopyDateName = ".$dest->sysTimeStamp; + $insflds[] = $this->RunInsertFilter($desttable,$dstCopyDateName, $p); $params[] = $dest->sysTimeStamp; + } + + + if (!empty($srcPKDest)) { + $selflds[] = $srcPKDest; + $fldoffsets = array($k+1); + } + + foreach($wheref as $uu => $fld) { + + $p = $dest->Param($k); + $sp = $src->Param($k); + if (!empty($srcuniqflds)) { + if ($uu > 1) die("Only one primary key for srcuniqflds allowed currently"); + $destsrckey = reset($srcuniqflds); + $wheres[] = reset($srcuniqflds).' = '.$p; + + $insflds[] = $this->RunInsertFilter($desttable,$destsrckey, $p); + $params[] = $p; + } else { + $wheres[] = $fld.' = '.$p; + if (!isset($ignoreflds[strtoupper($fld)]) || !empty($insertpkey)) { + $insflds[] = $this->RunInsertFilter($desttable,$fld, $p); + $params[] = $p; + } + } + + $selflds[] = $fld; + $srcwheres[] = $fld.' = '.$sp; + $fldoffsets[] = $k; + + $k++; + } + + if (!empty($srcPKDest)) { + $fldoffsets = array($k); + $srcwheres = array($fld.'='.$src->Param($k)); + $k++; + } + + if ($lastUpdateFld) { + $selflds[] = $lastUpdateFld; + } else + $selflds[] = 'null as Z55_DUMMY_LA5TUPD'; + + $insfldss = implode(', ', $insflds); + $fldss = implode(', ', $selflds); + $setss = implode(', ', $sets); + $paramss = implode(', ', $params); + $wheress = implode(' AND ', $wheres); + if (isset($srcwheres)) + $srcwheress = implode(' AND ',$srcwheres); + + + $seltable = $table; + if ($this->readUncommitted && strpos($src->databaseType,'mssql')) $seltable .= ' with (NOLOCK)'; + + $sa['SEL'] = "SELECT $fldss FROM $seltable $wheresrc"; + $sa['INS'] = "INSERT INTO $desttable ($insfldss) VALUES ($paramss) /**INS**/"; + $sa['UPD'] = "UPDATE $desttable SET $setss WHERE $wheress /**UPD**/"; + + + + $DB1 = "/* Source DB - sample sql in case you need to adapt code\n\n"; + $DB2 = "/* Dest DB - sample sql in case you need to adapt code\n\n"; + + if (!$this->execute) echo '/*
*/
+';
+		if ($deleteFirst && $this->deleteFirst) {
+			$where = preg_replace('/[ \n\r\t]+order[ \n\r\t]+by.*$/i', '', $where);
+			$sql = "DELETE FROM $desttable $wheredest\n";
+			if (!$this->execute) echo $DB2,'*/',$sql,"\n";
+			else $dest->Execute($sql);
+		}
+
+		global $ADODB_COUNTRECS;
+		$err = false;
+		$savemode = $src->setFetchMode(ADODB_FETCH_NUM);
+		$ADODB_COUNTRECS = false;
+
+		if (!$this->execute) {
+			echo $DB1,$sa['SEL'],"\n*/\n\n";
+			echo $DB2,$sa['INS'],"\n*/\n\n";
+			$suffix = ($onlyInsert) ? ' PRIMKEY=?' : '';
+			echo $DB2,$sa['UPD'],"$suffix\n*/\n\n";
+
+			$rs = $src->Execute($sa['SEL']);
+			$cnt = 1;
+			$upd = 0;
+			$ins = 0;
+
+			$sqlarr = explode('?',$sa['INS']);
+			$nparams = sizeof($sqlarr)-1;
+
+			$useQmark = $dest && ($dest->dataProvider != 'oci8');
+
+			while ($rs && !$rs->EOF) {
+				if ($useQmark) {
+					$sql = ''; $i = 0;
+					$arr = array_reverse($rs->fields);
+					//Use each() instead of foreach to reduce memory usage -mikefedyk
+					while(list(, $v) = each($arr)) {
+						$sql .= $sqlarr[$i];
+						// from Ron Baldwin 
+						// Only quote string types
+						$typ = gettype($v);
+						if ($typ == 'string')
+							//New memory copy of input created here -mikefedyk
+							$sql .= $dest->qstr($v);
+						else if ($typ == 'double')
+							$sql .= str_replace(',','.',$v); // locales fix so 1.1 does not get converted to 1,1
+						else if ($typ == 'boolean')
+							$sql .= $v ? $dest->true : $dest->false;
+						else if ($typ == 'object') {
+							if (method_exists($v, '__toString')) $sql .= $dest->qstr($v->__toString());
+							else $sql .= $dest->qstr((string) $v);
+						} else if ($v === null)
+							$sql .= 'NULL';
+						else
+							$sql .= $v;
+						$i += 1;
+
+						if ($i == $nparams) break;
+					} // while
+
+					if (isset($sqlarr[$i])) {
+						$sql .= $sqlarr[$i];
+					}
+					$INS = $sql;
+				} else {
+					$INS = $sa['INS'];
+					$arr = array_reverse($rs->fields);
+					foreach($arr as $k => $v) { // only works on oracle currently
+						$k = sizeof($arr)-$k-1;
+						$v = str_replace(":","%~%COLON%!%",$v);
+						$INS = str_replace(':'.$k,$this->fixupbinary($dest->qstr($v)),$INS);
+					}
+					$INS = str_replace("%~%COLON%!%",":",$INS);
+					if ($this->htmlSpecialChars) $INS = htmlspecialchars($INS);
+				}
+				echo "-- $cnt\n",$INS,";\n\n";
+				$cnt += 1;
+				$ins += 1;
+				$rs->MoveNext();
+			}
+			$src->setFetchMode($savemode);
+			return $sa;
+		} else {
+			$saved = $src->debug;
+			#$src->debug=1;
+			if ($this->limitRecs>100)
+				$rs = $src->SelectLimit($sa['SEL'],$this->limitRecs);
+			else
+				$rs = $src->Execute($sa['SEL']);
+			$src->debug = $saved;
+			if (!$rs) {
+				if ($this->errHandler) $this->_doerr('SEL',array());
+				return array(0,0,0,0);
+			}
+
+
+			if ($this->commitReplicate || $commitRecs > 0) {
+				$dest->BeginTrans();
+				if ($this->updateSrcFn) $src2->BeginTrans();
+			}
+
+			if ($this->updateSrcFn && strpos($src2->databaseType,'mssql') !== false) {
+				# problem is writers interfere with readers in mssql
+				$rs = $src->_rs2rs($rs);
+			}
+			$cnt = 0;
+			$upd = 0;
+			$ins = 0;
+
+			$sizeofrow = sizeof($selflds);
+
+			$fn = $this->selFilter;
+			$commitRecs = $this->commitRecs;
+
+			$saved = $dest->debug;
+
+			if ($this->deleteFirst) $onlyInsert = true;
+			while ($origrow = $rs->FetchRow()) {
+
+				if ($dest->debug) {flush(); @ob_flush();}
+
+				if ($fn) {
+					if (!$fn($desttable, $origrow, $deleteFirst, $this, $selflds)) continue;
+				}
+				$doinsert = true;
+				$row = array_slice($origrow,0,$sizeofrow-1);
+
+				if (!$onlyInsert) {
+					$doinsert = false;
+					$upderr = false;
+
+					if (isset($srcPKDest)) {
+						if (is_null($origrow[$sizeofrow-3])) {
+							$doinsert = true;
+							$upderr = true;
+						}
+					}
+					if (!$upderr && !$dest->Execute($sa['UPD'],$row)) {
+						$err = true;
+						$upderr = true;
+						if ($this->errHandler) $this->_doerr('UPD',$row);
+						if (!$this->neverAbort) break;
+					}
+
+				 	if ($upderr || $dest->Affected_Rows() == 0) {
+						$doinsert = true;
+					} else {
+						if (!empty($uniqflds)) $this->RunUpdateSrcFn($src2, $table, $fldoffsets, $origrow, $srcwheress, 'UPD', null, $lastUpdateFld);
+						$upd += 1;
+					}
+				}
+
+				if ($doinsert) {
+					$inserr = false;
+					if (isset($srcPKDest)) {
+						$row = array_slice($origrow,0,$sizeofrow-2);
+					}
+
+					if (! $dest->Execute($sa['INS'],$row)) {
+						$err = true;
+						$inserr = true;
+						if ($this->errHandler) $this->_doerr('INS',$row);
+						if ($this->neverAbort) continue;
+						else break;
+					} else {
+						if ($dest->dataProvider == 'oci8') {
+							if ($this->oracleSequence) $lastid = $dest->GetOne("select ".$this->oracleSequence.".currVal from dual");
+						 	else $lastid = 'null';
+						} else {
+							$lastid = $dest->Insert_ID();
+						}
+
+						if (!$inserr && !empty($uniqflds)) {
+							$this->RunUpdateSrcFn($src2, $table, $fldoffsets, $origrow, $srcwheress, 'INS', $lastid,$lastUpdateFld);
+						}
+						$ins += 1;
+					}
+				}
+				$cnt += 1;
+
+				if ($commitRecs > 0 && ($cnt % $commitRecs) == 0) {
+					$dest->CommitTrans();
+					$dest->BeginTrans();
+
+					if ($this->updateSrcFn) {
+						$src2->CommitTrans();
+						$src2->BeginTrans();
+					}
+				}
+
+			} // while
+
+
+			if ($this->commitReplicate || $commitRecs > 0) {
+				if (!$this->neverAbort && $err) {
+					$dest->RollbackTrans();
+					if ($this->updateSrcFn) $src2->RollbackTrans();
+				} else {
+					$dest->CommitTrans();
+					if ($this->updateSrcFn) $src2->CommitTrans();
+				}
+			}
+		}
+		if ($cnt != $ins + $upd) echo "

ERROR: $cnt != INS $ins + UPD $upd

"; + $src->setFetchMode($savemode); + return array(!$err, $cnt, $ins, $upd); + } + // trigger support only for sql server and oracle + // need to add + function MergeSrcSetup($srcTable, $pkeys, $srcUpdateDateFld, $srcCopyDateFld, $srcCopyFlagFld, + $srcCopyFlagType='C(1)', $srcCopyFlagVals = array('Y','N','P','=')) + { + $sqla = array(); + $src = $this->connSrc; + $idx = $srcTable.'_mrgIdx'; + $cols = $src->MetaColumns($srcTable); + #adodb_pr($cols); + if (!isset($cols[strtoupper($srcUpdateDateFld)])) { + $sqla = $this->ddSrc->AddColumnSQL($srcTable, "$srcUpdateDateFld TS DEFTIMESTAMP"); + foreach($sqla as $sql) $src->Execute($sql); + } + + if ($srcCopyDateFld && !isset($cols[strtoupper($srcCopyDateFld)])) { + $sqla = $this->ddSrc->AddColumnSQL($srcTable, "$srcCopyDateFld TS DEFTIMESTAMP"); + foreach($sqla as $sql) $src->Execute($sql); + } + + $sysdate = $src->sysTimeStamp; + $arrv0 = $src->qstr($srcCopyFlagVals[0]); + $arrv1 = $src->qstr($srcCopyFlagVals[1]); + $arrv2 = $src->qstr($srcCopyFlagVals[2]); + $arrv3 = $src->qstr($srcCopyFlagVals[3]); + + if ($srcCopyFlagFld && !isset($cols[strtoupper($srcCopyFlagFld)])) { + $sqla = $this->ddSrc->AddColumnSQL($srcTable, "$srcCopyFlagFld $srcCopyFlagType DEFAULT $arrv1"); + foreach($sqla as $sql) $src->Execute($sql); + } + + $sqla = array(); + + + $name = "{$srcTable}_mrgTr"; + if (is_array($pkeys) && strpos($src->databaseType,'mssql') !== false) { + $pk = reset($pkeys); + + #$sqla[] = "DROP TRIGGER $name"; + $sqltr = " + TRIGGER $name + ON $srcTable /* for data replication and merge */ + AFTER UPDATE + AS + UPDATE $srcTable + SET + $srcUpdateDateFld = case when I.$srcCopyFlagFld = $arrv2 or I.$srcCopyFlagFld = $arrv3 then I.$srcUpdateDateFld + else $sysdate end, + $srcCopyFlagFld = case + when I.$srcCopyFlagFld = $arrv2 then $arrv0 + when I.$srcCopyFlagFld = $arrv3 then D.$srcCopyFlagFld + else $arrv1 end + FROM $srcTable S Join Inserted AS I on I.$pk = S.$pk + JOIN Deleted as D ON I.$pk = D.$pk + WHERE I.$srcCopyFlagFld = D.$srcCopyFlagFld or I.$srcCopyFlagFld = $arrv2 + or I.$srcCopyFlagFld = $arrv3 or I.$srcCopyFlagFld is null + "; + $sqla[] = 'CREATE '.$sqltr; // first if does not exists + $sqla[] = 'ALTER '.$sqltr; // second if it already exists + } else if (strpos($src->databaseType,'oci') !== false) { + + if (strlen($srcTable)>22) $tableidx = substr($srcTable,0,16).substr(crc32($srcTable),6); + else $tableidx = $srcTable; + + $name = $tableidx.$this->trgSuffix; + $idx = $tableidx.$this->idxSuffix; + $sqla[] = " +CREATE OR REPLACE TRIGGER $name /* for data replication and merge */ +BEFORE UPDATE ON $srcTable REFERENCING NEW AS NEW OLD AS OLD +FOR EACH ROW +BEGIN + if :new.$srcCopyFlagFld = $arrv2 then + :new.$srcCopyFlagFld := $arrv0; + elsif :new.$srcCopyFlagFld = $arrv3 then + :new.$srcCopyFlagFld := :old.$srcCopyFlagFld; + elsif :old.$srcCopyFlagFld = :new.$srcCopyFlagFld or :new.$srcCopyFlagFld is null then + if $this->trLogic then + :new.$srcUpdateDateFld := $sysdate; + :new.$srcCopyFlagFld := $arrv1; + end if; + end if; +END; +"; + } + foreach($sqla as $sql) $src->Execute($sql); + + if ($srcCopyFlagFld) $srcCopyFlagFld .= ', '; + $src->Execute("CREATE INDEX {$idx} on $srcTable ($srcCopyFlagFld$srcUpdateDateFld)"); + } + + + /* + Perform Merge by copying all data modified from src to dest + then update src copied flag if present. + + Returns array taken from ReplicateData: + + Returns an array: + $arr[0] = true if no error, false if error + $arr[1] = number of recs processed + $arr[2] = number of successful inserts + $arr[3] = number of successful updates + + $srcTable = src table + $dstTable = dest table + $pkeys = primary keys array. if empty, then only inserts will occur + $srcignoreflds = ignore these flds (must be upper cased) + $setsrc = updateSrcFn string + $srcUpdateDateFld = field in src with the last update date + $srcCopyFlagFld = false = optional field that holds the copied indicator + $flagvals=array('Y','N','P','=') = array of values indicating array(copied, not copied). + Null is assumed to mean not copied. The 3rd value 'P' indicates that we want to force 'Y', bypassing + default trigger behaviour to reset the COPIED='N' when the record is replicated from other side. + The last value '=' is don't change copyflag. + $srcCopyDateFld = field that holds last copy date in src table, which will be updated on Merge() + $dstCopyDateFld = field that holds last copy date in dst table, which will be updated on Merge() + $defaultDestRaiseErrorFn = The adodb raiseErrorFn handler. Default is to not raise an error. + Just output error message to stdout + + */ + + + function Merge($srcTable, $dstTable, $pkeys, $srcignoreflds, $setsrc, + $srcUpdateDateFld, + $srcCopyFlagFld, $flagvals=array('Y','N','P','='), + $srcCopyDateFld = false, + $dstCopyDateFld = false, + $whereClauses = '', + $orderBy = '', # MUST INCLUDE THE "ORDER BY" suffix + $copyDoneFlagIdx = 3, + $defaultDestRaiseErrorFn = '') + { + $src = $this->connSrc; + $dest = $this->connDest; + + $time = $src->Time(); + + $delfirst = $this->deleteFirst; + $upd = $this->updateSrcFn; + + $this->deleteFirst = false; + //$this->updateFirst = true; + + $srcignoreflds[] = $srcUpdateDateFld; + $srcignoreflds[] = $srcCopyFlagFld; + $srcignoreflds[] = $srcCopyDateFld; + + if (empty($whereClauses)) $whereClauses = '1=1'; + $where = " WHERE ($whereClauses) and ($srcCopyFlagFld = ".$src->qstr($flagvals[1]).')'; + if ($orderBy) $where .= ' '.$orderBy; + else $where .= ' ORDER BY '.$srcUpdateDateFld; + + if ($setsrc) $set[] = $setsrc; + else $set = array(); + + if ($srcCopyFlagFld) $set[] = "$srcCopyFlagFld = ".$src->qstr($flagvals[2]); + if ($srcCopyDateFld) $set[]= "$srcCopyDateFld = ".$src->sysTimeStamp; + if ($set) $this->updateSrcFn = array(implode(', ',$set)); + else $this->updateSrcFn = ''; + + + $extra[$srcCopyFlagFld] = array($dest->qstr($flagvals[0]),$dest->qstr($flagvals[$copyDoneFlagIdx])); + + $saveraise = $dest->raiseErrorFn; + $dest->raiseErrorFn = ''; + + if ($this->compat && $this->compat == 1.0) $srcUpdateDateFld = ''; + $arr = $this->ReplicateData($srcTable, $dstTable, $pkeys, $where, $srcignoreflds, + $dstCopyDateFld,$extra,$srcUpdateDateFld); + + $dest->raiseErrorFn = $saveraise; + + $this->updateSrcFn = $upd; + $this->deleteFirst = $delfirst; + + return $arr; + } + /* + If doing a 2 way merge, then call + $rep->Merge() + to save without modifying the COPIEDFLAG ('='). + + Then can the following to set the COPIEDFLAG to 'P' which forces the COPIEDFLAG = 'Y' + $rep->MergeDone() + */ + + function MergeDone($srcTable, $dstTable, $pkeys, $srcignoreflds, $setsrc, + $srcUpdateDateFld, + $srcCopyFlagFld, $flagvals=array('Y','N','P','='), + $srcCopyDateFld = false, + $dstCopyDateFld = false, + $whereClauses = '', + $orderBy = '', # MUST INCLUDE THE "ORDER BY" suffix + $copyDoneFlagIdx = 2, + $defaultDestRaiseErrorFn = '') + { + return $this->Merge($srcTable, $dstTable, $pkeys, $srcignoreflds, $setsrc, + $srcUpdateDateFld, + $srcCopyFlagFld, $flagvals, + $srcCopyDateFld, + $dstCopyDateFld, + $whereClauses, + $orderBy, # MUST INCLUDE THE "ORDER BY" suffix + $copyDoneFlagIdx, + $defaultDestRaiseErrorFn); + } + + function _doerr($reason, $selflds) + { + $fn = $this->errHandler; + if ($fn) $fn($this, $reason, $selflds); // set $this->neverAbort to true or false as required inside $fn + } +} diff --git a/app/vendor/adodb/adodb-php/replicate/replicate-steps.php b/app/vendor/adodb/adodb-php/replicate/replicate-steps.php new file mode 100644 index 000000000..5b696568d --- /dev/null +++ b/app/vendor/adodb/adodb-php/replicate/replicate-steps.php @@ -0,0 +1,137 @@ +Connect($HOST,$USER,$PWD,$DBASE); +if (!$ok) return; + + +#$DB->debug=1; + +$bkup = 'tmp'.date('ymd_His'); + + +if ($BA) { + $QTY_BA = " and qu_bacode='$BA'"; + if (1) $STP_BA = " and s_stagecat in (select stg_stagecat from kbstage where stg_bacode='$BA')"; # OLDER KBSTEP + else $STP_BA = " and s_bacode='$BA'"; # LATEST KBSTEP format + $STG_BA = " and stg_bacode='$BA'"; +} else { + $QTY_BA = ""; + $STP_BA = ""; + $STG_BA = ""; +} + +if ($STAGES) { + + $STAGES = explode(',',$STAGES); + $STAGES = "'".implode("','",$STAGES)."'"; + $QTY_STG = " and qu_stagecat in ($STAGES)"; + $STP_STG = " and s_stagecat in ($STAGES)"; + $STG_STG = " and stg_stagecat in ($STAGES)"; +} else { + $QTY_STG = ""; + $STP_STG = ""; + $STG_STG = ""; +} + +echo "
+
+/******************************************************************************
+
+ Migrate stages, steps and qtypes for the following
+
+  business area: $BA
+     and stages: $STAGES
+
+
+ WARNING: DO NOT 'Ignore All Errors'.
+ If any error occurs, make sure you stop and check the reason and fix it.
+ Otherwise you could corrupt everything!!!
+
+ Connected to $USER@$DBASE $HOST;
+
+*******************************************************************************/
+
+-- BACKUP
+create table kbstage_$bkup as select * from kbstage;
+create table kbstep_$bkup as select * from kbstep;
+create table kbqtype_$bkup as select * from kbqtype;
+
+
+-- IF CODE FAILS, REMEMBER TO RENABLE ALL TRIGGERS and following CONSTRAINT
+ALTER TABLE kbstage DISABLE all triggers;
+ALTER TABLE kbstep DISABLE all triggers;
+ALTER TABLE kbqtype DISABLE all triggers;
+ALTER TABLE jqueue DISABLE CONSTRAINT QUEUE_MUST_HAVE_TYPE;
+
+
+-- NOW DELETE OLD STEPS/STAGES/QUEUES
+delete from kbqtype where qu_mode in ('STAGE','STEP') $QTY_BA $QTY_STG;
+delete from kbstep where (1=1) $STP_BA$STP_STG;
+delete from kbstage where (1=1)$STG_BA$STG_STG;
+
+
+
+SET DEFINE OFF; -- disable  variable handling by sqlplus
+/
+/* Assume kbstrategy and business areas are compatible for steps and stages to be copied */
+
+ +"; + + +$rep = new ADODB_Replicate($DB,$DB); +$rep->execute = false; +$rep->deleteFirst = false; + + // src table name, dst table name, primary key, where condition +$rep->ReplicateData('KBSTAGE', 'KBSTAGE', array(), " where (1=1)$STG_BA$STG_STG"); +$rep->ReplicateData('KBSTEP', 'KBSTEP', array(), " where (1=1)$STP_BA$STP_STG"); +$rep->ReplicateData('KBQTYPE','KBQTYPE',array()," where qu_mode in ('STAGE','STEP')$QTY_BA$QTY_STG"); + +echo " + +-- Check for QUEUES not in KBQTYPE and FIX by copying from kbqtype_$bkup +begin +for rec in (select distinct q_type from jqueue where q_type not in (select qu_code from kbqtype)) loop + insert into kbqtype select * from kbqtype_$bkup where qu_code = rec.q_type; + update kbqtype set qu_name=substr('MISSING.'||qu_name,1,64) where qu_code=rec.q_type; +end loop; +end; +/ + +commit; + + +ALTER TABLE kbstage ENABLE all triggers; +ALTER TABLE kbstep ENABLE all triggers; +ALTER TABLE kbqtype ENABLE all triggers; +ALTER TABLE jqueue ENABLE CONSTRAINT QUEUE_MUST_HAVE_TYPE; + +/* +-- REMEMBER TO COMMIT + commit; + begin Juris.UpdateQCounts; end; + +-- To check for bad queues after conversion, run this + select * from kbqtype where qu_name like 'MISSING%' +*/ +/ +"; diff --git a/app/vendor/adodb/adodb-php/replicate/test-tnb.php b/app/vendor/adodb/adodb-php/replicate/test-tnb.php new file mode 100644 index 000000000..f163ff4f5 --- /dev/null +++ b/app/vendor/adodb/adodb-php/replicate/test-tnb.php @@ -0,0 +1,421 @@ + 28) $idxname = substr($idxname,0,24).rand(1000,9999); + return $idxname; +} + +function SelFilter($table, &$arr, $delfirst) +{ + return true; +} + +function updatefilter($table, $fld, $val) +{ + return "nvl($fld, $val)"; +} + + +function FieldFilter(&$fld,$mode) +{ + $uf = strtoupper($fld); + switch($uf) { + case 'SIZEFLD': + return 'Size'; + + case 'GROUPFLD': + return 'Group'; + + case 'GROUP': + if ($mode == 'SELECT') $fld = '"Group"'; + return 'GroupFld'; + case 'SIZE': + if ($mode == 'SELECT') $fld = '"Size"'; + return 'SizeFld'; + } + return $fld; +} + +function ParseTable(&$table, &$pkey) +{ + $table = trim($table); + if (strlen($table) == 0) return false; + if (strpos($table, '#') !== false) { + $at = strpos($table, '#'); + $table = trim(substr($table,0,$at)); + if (strlen($table) == 0) return false; + } + + $tabarr = explode(',',$table); + if (sizeof($tabarr) == 1) { + $table = $tabarr[0]; + $pkey = ''; + echo "No primary key for $table **** ****
"; + } else { + $table = trim($tabarr[0]); + $pkey = trim($tabarr[1]); + if (strpos($pkey,' ') !== false) echo "Bad PKEY for $table $pkey
"; + } + + return true; +} + +global $TARR; + +function TableStats($rep, $table, $pkey) +{ +global $TARR; + + if (empty($TARR)) $TARR = array(); + $cnt = $rep->connSrc->GetOne("select count(*) from $table"); + if (isset($TARR[$table])) echo "

Table $table repeated twice

"; + $TARR[$table] = $cnt; + + if ($pkey) { + $ok = $rep->connSrc->SelectLimit("select $pkey from $table",1); + if (!$ok) echo "

$table: $pkey does not exist

"; + } else + echo "

$table: no primary key

"; +} + +function CreateTable($rep, $table) +{ +## CREATE TABLE + #$DB2->Execute("drop table $table"); + + $rep->execute = true; + $ok = $rep->CopyTableStruct($table); + if ($ok) echo "Table Created
\n"; + else { + echo "
Error: Cannot Create Table
\n"; + } + flush();@ob_flush(); +} + +function CopyData($rep, $table, $pkey) +{ + $dtable = $table; + + $rep->execute = true; + $rep->deleteFirst = true; + + $secs = time(); + $rows = $rep->ReplicateData($table,$dtable,array($pkey)); + $secs = time() - $secs; + if (!$rows || !$rows[0] || !$rows[1] || $rows[1] != $rows[2]+$rows[3]) { + echo "
Error: "; var_dump($rows); echo " (secs=$secs)
\n"; + } else + echo date('H:i:s'),': ',$rows[1]," record(s) copied, ",$rows[2]," inserted, ",$rows[3]," updated (secs=$secs)
\n"; + flush();@ob_flush(); +} + +function MergeDataJohnTest($rep, $table, $pkey) +{ + $rep->SwapDBs(); + + $dtable = $table; + $rep->oracleSequence = 'LGBSEQUENCE'; + +# $rep->MergeSrcSetup($table, array($pkey),'UpdatedOn','CopiedFlag'); + if (strpos($rep->connDest->databaseType,'mssql') !== false) { # oracle ==> mssql + $ignoreflds = array($pkey); + $ignoreflds[] = 'MSSQL_ID'; + $set = 'MSSQL_ID=nvl($INSERT_ID,MSSQL_ID)'; + $pkeyarr = array(array($pkey),false,array('MSSQL_ID'));# array('MSSQL_ID', 'ORA_ID')); + } else { # mssql ==> oracle + $ignoreflds = array($pkey); + $ignoreflds[] = 'ORA_ID'; + $set = ''; + #$set = 'ORA_ID=isnull($INSERT_ID,ORA_ID)'; + $pkeyarr = array(array($pkey),array('MSSQL_ID')); + } + $rep->execute = true; + #$rep->updateFirst = false; + $ok = $rep->Merge($table, $dtable, $pkeyarr, $ignoreflds, $set, 'UpdatedOn','CopiedFlag',array('Y','N','P','='), 'CopyDate'); + var_dump($ok); + + #$rep->connSrc->Execute("update JohnTest set name='Apple' where id=4"); +} + +$DB = ADONewConnection('odbtp'); +#$ok = $DB->Connect('localhost','root','','northwind'); +$ok = $DB->Connect('192.168.0.1','DRIVER={SQL Server};SERVER=(local);UID=sa;PWD=natsoft;DATABASE=OIR;','',''); +$DB->_bindInputArray = false; + +$DB2 = ADONewConnection('oci8'); +$ok2 = $DB2->Connect('192.168.0.2','tnb','natsoft','RAPTOR',''); + +if (!$ok || !$ok2) die("Failed connection DB=$ok DB2=$ok2
"); + +$tables = +" +JohnTest,id +"; + +# net* are ERMS, need last updated field from LGBnet +# tblRep* are tables insert or update from Juris, need last updated field also +# The rest are lookup tables, can copy all from LGBnet + +$tablesOrig = +" +SysVoltSubLevel,id +# Lookup table for Restoration Details screen +sysefi,ID # (not identity) +sysgenkva,ID #(not identity) +sysrestoredby,ID #(not identity) +# Sel* table added on 24 Oct +SELSGManufacturer,ID +SelABCCondSizeLV,ID +SelABCCondSizeMV,ID +SelArchingHornSize,ID +SelBallastSize,ID +SelBallastType,ID +SelBatteryType,ID #(not identity) +SelBreakerCapacity,ID +SelBreakerType,ID #(not identity) +SelCBreakerManuf,ID +SelCTRatio,ID #(not identity) +SelCableBrand,ID +SelCableSize,ID +SelCableSizeLV,ID # (not identity) +SelCapacitorSize,ID +SelCapacitorType,ID +SelColourCode,ID +SelCombineSealingChamberSize,ID +SelConductorBrand,ID +SelConductorSize4,ID +SelConductorSizeLV,ID +SelConductorSizeMV,ID +SelContactorSize,ID +SelContractor,ID +SelCoverType,ID +SelCraddleSize,ID +SelDeadEndClampBrand,ID +SelDeadEndClampSize,ID +SelDevTermination,ID +SelFPManuf,ID +SelFPillarRating,ID +SelFalseTrue,ID +SelFuseManuf,ID +SelFuseType,ID +SelIPCBrand,ID +SelIPCSize,ID +SelIgnitorSize,ID +SelIgnitorType,ID +SelInsulatorBrand,ID +SelJoint,ID +SelJointBrand,ID +SelJunctionBoxBrand,ID +SelLVBoardBrand,ID +SelLVBoardSize,ID +SelLVOHManuf,ID +SelLVVoltage,ID +SelLightningArresterBrand,ID +SelLightningShieldwireSize,ID +SelLineTapSize,ID +SelLocation,ID +SelMVVoltage,ID +SelMidSpanConnectorsSize,ID +SelMidSpanJointSize,ID +SelNERManuf,ID +SelNERType,ID +SelNLinkSize,ID +SelPVCCondSizeLV,ID +SelPoleBrand,ID +SelPoleConcreteSize,ID +SelPoleSize,ID +SelPoleSpunConcreteSize,ID +SelPoleSteelSize,ID +SelPoleType,ID +SelPoleWoodSize,ID +SelPorcelainFuseSize,ID +SelRatedFaultCurrentBreaker,ID +SelRatedVoltageSG,ID #(not identity) +SelRelayType,ID # (not identity) +SelResistanceValue,ID +SelSGEquipmentType,ID # (not identity) +SelSGInsulationType,ID # (not identity) +SelSGManufacturer,ID +SelStayInsulatorSize,ID +SelSuspensionClampBrand,ID +SelSuspensionClampSize,ID +SelTSwitchType,ID +SelTowerType,ID +SelTransformerCapacity,ID +SelTransformerManuf,ID +SelTransformerType,ID #(not identity) +SelTypeOfArchingHorn,ID +SelTypeOfCable,ID #(not identity) +SelTypeOfConductor,ID # (not identity) +SelTypeOfInsulationCB,ID # (not identity) +SelTypeOfMidSpanJoint,ID +SelTypeOfSTJoint,ID +SelTypeSTCable,ID +SelUGVoltage,ID # (not identity) +SelVoltageInOut,ID +SelWireSize,ID +SelWireType,ID +SelWonpieceBrand,ID +# +# Net* tables added on 24 Oct +NetArchingHorn,Idx +NetBatteryBank,Idx # identity, FunctLocation Pri +NetBiMetal,Idx +NetBoxFuse,Idx +NetCable,Idx # identity, FunctLocation Pri +NetCapacitorBank,Idx # identity, FunctLocation Pri +NetCircuitBreaker,Idx # identity, FunctLocation Pri +NetCombineSealingChamber,Idx +NetCommunication,Idx +NetCompInfras,Idx +NetControl,Idx +NetCraddle,Idx +NetDeadEndClamp,Idx +NetEarthing,Idx +NetFaultIndicator,Idx +NetFeederPillar,Idx # identity, FunctLocation Pri +NetGenCable,Idx # identity , FunctLocation Not Null +NetGenerator,Idx +NetGrid,Idx +NetHVOverhead,Idx #identity, FunctLocation Pri +NetHVUnderground,Idx #identity, FunctLocation Pri +NetIPC,Idx +NetInductorBank,Idx +NetInsulator,Idx +NetJoint,Idx +NetJunctionBox,Idx +NetLVDB,Idx #identity, FunctLocation Pri +NetLVOverhead,Idx +NetLVUnderground,Idx # identity, FunctLocation Not Null +NetLightningArrester,Idx +NetLineTap,Idx +NetMidSpanConnectors,Idx +NetMidSpanJoint,Idx +NetNER,Idx # identity , FunctLocation Pri +NetOilPump,Idx +NetOtherComponent,Idx +NetPole,Idx +NetRMU,Idx # identity, FunctLocation Pri +NetStreetLight,Idx +NetStrucSupp,Idx +NetSuspensionClamp,Idx +NetSwitchGear,Idx # identity, FunctLocation Pri +NetTermination,Idx +NetTransition,Idx +NetWonpiece,Idx +# +# comment1 +SelMVFuseType,ID +selFuseSize,ID +netRelay,Idx # identity, FunctLocation Pri +SysListVolt,ID +sysVoltLevel,ID_SVL +sysRestoration,ID_SRE +sysRepairMethod,ID_SRM # (not identity) + +sysInterruptionType,ID_SIN +netTransformer,Idx # identity, FunctLocation Pri +# +# +sysComponent,ID_SC +sysCodecibs #-- no idea, UpdatedOn(the only column is unique),Ermscode,Cibscode is unique but got null value +sysCodeno,id +sysProtection,ID_SP +sysEquipment,ID_SEQ +sysAddress #-- no idea, ID_SAD(might be auto gen No) +sysWeather,ID_SW +sysEnvironment,ID_SE +sysPhase,ID_SPH +sysFailureCause,ID_SFC +sysFailureMode,ID_SFM +SysSchOutageMode,ID_SSM +SysOutageType,ID_SOT +SysInstallation,ID_SI +SysInstallationCat,ID_SIC +SysInstallationType,ID_SIT +SysFaultCategory,ID_SF #(not identity) +SysResponsible,ID_SR +SysProtectionOperation,ID_SPO #(not identity) +netCodename,CodeNo #(not identity) +netSubstation,Idx #identity, FunctLocation Pri +netLvFeeder,Idx # identity, FunctLocation Pri +# +# +tblReport,ReportNo +tblRepRestoration,ID_RR +tblRepResdetail,ID_RRD +tblRepFailureMode,ID_RFM +tblRepFailureCause,ID_RFC +tblRepRepairMethod,ReportNo # (not identity) +tblInterruptionType,ID_TIN +tblProtType,ID_PT #--capital letter +tblRepProtection,ID_RP +tblRepComponent,ID_RC +tblRepWeather,ID_RW +tblRepEnvironment,ID_RE +tblRepSubstation,ID_RSS +tblInstallationType,ID_TIT +tblInstallationCat,ID_TIC +tblFailureCause,ID_TFC +tblFailureMode,ID_TFM +tblProtection,ID_TP +tblComponent,ID_TC +tblProtdetail,Id # (Id)--capital letter for I +tblInstallation,ID_TI +# +"; + + +$tables = explode("\n",$tables); + +$rep = new ADODB_Replicate($DB,$DB2); +$rep->fieldFilter = 'FieldFilter'; +$rep->selFilter = 'SELFILTER'; +$rep->indexFilter = 'IndexFilter'; + +if (1) { + $rep->debug = 1; + $DB->debug=1; + $DB2->debug=1; +} + +# $rep->SwapDBs(); + +$cnt = sizeof($tables); +foreach($tables as $k => $table) { + $pkey = ''; + if (!ParseTable($table, $pkey)) continue; + + ####################### + + $kcnt = $k+1; + echo "

($kcnt/$cnt) $table -- $pkey

\n"; + flush();@ob_flush(); + + CreateTable($rep,$table); + + + # COPY DATA + + + TableStats($rep, $table, $pkey); + + if ($table == 'JohnTest') MergeDataJohnTest($rep, $table, $pkey); + else CopyData($rep, $table, $pkey); + +} + + +if (!empty($TARR)) { + ksort($TARR); + adodb_pr($TARR); + asort($TARR); + adodb_pr($TARR); +} + +echo "
",date('H:i:s'),": Done"; diff --git a/app/vendor/adodb/adodb-php/rsfilter.inc.php b/app/vendor/adodb/adodb-php/rsfilter.inc.php new file mode 100644 index 000000000..4b609b98b --- /dev/null +++ b/app/vendor/adodb/adodb-php/rsfilter.inc.php @@ -0,0 +1,62 @@ + $v) { + $arr[$k] = ucwords($v); + } + } + $rs = RSFilter($rs,'do_ucwords'); + */ +function RSFilter($rs,$fn) +{ + if ($rs->databaseType != 'array') { + if (!$rs->connection) return false; + + $rs = $rs->connection->_rs2rs($rs); + } + $rows = $rs->RecordCount(); + for ($i=0; $i < $rows; $i++) { + if (is_array ($fn)) { + $obj = $fn[0]; + $method = $fn[1]; + $obj->$method ($rs->_array[$i],$rs); + } else { + $fn($rs->_array[$i],$rs); + } + + } + if (!$rs->EOF) { + $rs->_currentRow = 0; + $rs->fields = $rs->_array[0]; + } + + return $rs; +} diff --git a/app/vendor/adodb/adodb-php/scripts/.gitignore b/app/vendor/adodb/adodb-php/scripts/.gitignore new file mode 100644 index 000000000..3e0e62b20 --- /dev/null +++ b/app/vendor/adodb/adodb-php/scripts/.gitignore @@ -0,0 +1,2 @@ +# Python byte code +*.pyc diff --git a/app/vendor/adodb/adodb-php/scripts/TARADO5.BAT b/app/vendor/adodb/adodb-php/scripts/TARADO5.BAT new file mode 100644 index 000000000..bf25ef990 --- /dev/null +++ b/app/vendor/adodb/adodb-php/scripts/TARADO5.BAT @@ -0,0 +1,49 @@ +@rem REQUIRES P:\INSTALLS\CMDUTILS + +echo Don't forget to strip LF's !!!!!!!!!!! +pause + + +set VER=518a + +d: +cd \inetpub\wwwroot\php + +@del /s /q zadodb\*.* +@mkdir zadodb + +@REM not for release -- make sure in VSS +attrib -r adodb5\drivers\adodb-text.inc.php +del adodb5\*.bak +del adodb5\drivers\*.bak +del adodb5\hs~*.* +del adodb5\drivers\hs~*.* +del adodb5\tests\hs~*.* +del adodb5\drivers\adodb-text.inc.php +del adodb5\.#* +del adodb5\replicate\replicate-steps.php +del adodb5\replicate\test*.php +del adodb5\adodb-lite.inc.php +attrib -r adodb5\*.php +del adodb5\cute_icons_for_site\*.png + +del tmp.tar +del adodb5*.tgz +del adodb5*.zip + +@mkdir adodb5\docs +move /y adodb5\*.htm adodb5\docs + +@rem CREATE TAR FILE +tar -f adodb%VER%.tar -c adodb5/*.* adodb5/perf/*.* adodb5/session/*.* adodb5/pear/*.txt adodb5/pear/Auth/Container/ADOdb.php adodb5/session/old/*.* adodb5/drivers/*.* adodb5/lang/*.* adodb5/tests/*.* adodb5/cute_icons_for_site/*.* adodb5/datadict/*.* adodb5/contrib/*.* adodb5/xsl/*.* adodb5/docs/*.* + +@rem CREATE ZIP FILE +cd zadodb +tar -xf ..\adodb%VER%.TAR +zip -r ..\adodb%VER%.zip adodb5 +cd .. + +@rem CREATE TGZ FILE, THE RENAME CHANGES UPPERCASE TO LOWERCASE +gzip -v ADODB%VER%.tar -S .tgz -9 +rename ADODB%VER%.tar.TGZ adodb%VER%.tgz + diff --git a/app/vendor/adodb/adodb-php/scripts/buildrelease.py b/app/vendor/adodb/adodb-php/scripts/buildrelease.py new file mode 100755 index 000000000..0b37b97b6 --- /dev/null +++ b/app/vendor/adodb/adodb-php/scripts/buildrelease.py @@ -0,0 +1,270 @@ +#!/usr/bin/python -u +''' + ADOdb release build script + + - Create release tag if it does not exist + - Copy release files to target directory + - Generate zip/tar balls + - +''' + +import errno +import getopt +import re +import os +from os import path +import shutil +import subprocess +import sys +import tempfile + +import updateversion + + +# ADOdb Repository reference +origin_repo = "https://github.com/ADOdb/ADOdb.git" +release_branch = "master" +release_prefix = "adodb" + +# Directories and files to exclude from release tarballs +exclude_list = (".git*", + "replicate", + "scripts", + "tests", + # There are no png files in there... + # "cute_icons_for_site/*.png", + "hs~*.*", + "adodb-text.inc.php", + # This file does not exist in current repo + # 'adodb-lite.inc.php' + ) + +# Command-line options +options = "hb:dfk" +long_options = ["help", "branch", "debug", "fresh", "keep"] + +# Global flags +debug_mode = False +fresh_clone = False +cleanup = True + + +def usage(): + print '''Usage: %s [options] version release_path + + Parameters: + version ADOdb version to bundle (e.g. v5.19) + release_path Where to save the release tarballs + + Options: + -h | --help Show this usage message + + -b | --branch Use specified branch (defaults to '%s' for '.0' + releases, or 'hotfix/' for patches) + -d | --debug Debug mode (ignores upstream: no fetch, allows + build even if local branch is not in sync) + -f | --fresh Create a fresh clone of the repository + -k | --keep Keep build directories after completion + (useful for debugging) +''' % ( + path.basename(__file__), + release_branch + ) +#end usage() + + +def set_version_and_tag(version): + ''' + ''' + global release_branch, debug_mode, fresh_clone, cleanup + + # Delete existing tag to force creation in debug mode + if debug_mode: + try: + updateversion.tag_delete(version) + except: + pass + + # Checkout release branch + subprocess.call("git checkout %s" % release_branch, shell=True) + + if not debug_mode: + # Make sure we're up-to-date, ignore untracked files + ret = subprocess.check_output( + "git status --branch --porcelain --untracked-files=no", + shell=True + ) + if not re.search(release_branch + "$", ret): + print "\nERROR: branch must be aligned with upstream" + sys.exit(4) + + # Update the code, create commit and tag + updateversion.version_set(version) + + # Make sure we don't delete the modified repo + if fresh_clone: + cleanup = False + + +def main(): + global release_branch, debug_mode, fresh_clone, cleanup + + # Get command-line options + try: + opts, args = getopt.gnu_getopt(sys.argv[1:], options, long_options) + except getopt.GetoptError, err: + print str(err) + usage() + sys.exit(2) + + if len(args) < 2: + usage() + print "ERROR: please specify the version and release_path" + sys.exit(1) + + for opt, val in opts: + if opt in ("-h", "--help"): + usage() + sys.exit(0) + + elif opt in ("-b", "--branch"): + release_branch = val + + elif opt in ("-d", "--debug"): + debug_mode = True + + elif opt in ("-f", "--fresh"): + fresh_clone = True + + elif opt in ("-k", "--keep"): + cleanup = False + + # Mandatory parameters + version = updateversion.version_check(args[0]) + release_path = args[1] + + # Default release branch + if updateversion.version_is_patch(version): + release_branch = 'hotfix/' + version + + # ------------------------------------------------------------------------- + # Start the build + # + global release_prefix + + print "Building ADOdb release %s into '%s'\n" % ( + version, + release_path + ) + + if debug_mode: + print "DEBUG MODE: ignoring upstream repository status" + + if fresh_clone: + # Create a new repo clone + print "Cloning a new repository" + repo_path = tempfile.mkdtemp(prefix=release_prefix + "-", + suffix=".git") + subprocess.call( + "git clone %s %s" % (origin_repo, repo_path), + shell=True + ) + os.chdir(repo_path) + else: + repo_path = subprocess.check_output('git root', shell=True).rstrip() + os.chdir(repo_path) + + # Check for any uncommitted changes + try: + subprocess.check_output( + "git diff --exit-code && " + "git diff --cached --exit-code", + shell=True + ) + except: + print "ERROR: there are uncommitted changes in the repository" + sys.exit(3) + + # Update the repository + if not debug_mode: + print "Updating repository in '%s'" % os.getcwd() + try: + subprocess.check_output("git fetch", shell=True) + except: + print "ERROR: unable to fetch\n" + sys.exit(3) + + # Check existence of Tag for version in repo, create if not found + try: + updateversion.tag_check(version) + if debug_mode: + set_version_and_tag(version) + except: + set_version_and_tag(version) + + # Copy files to release dir + release_files = release_prefix + version.split(".")[0] + release_tmp_dir = path.join(release_path, release_files) + print "Copying release files to '%s'" % release_tmp_dir + retry = True + while True: + try: + shutil.copytree( + repo_path, + release_tmp_dir, + ignore=shutil.ignore_patterns(*exclude_list) + ) + break + except OSError, err: + # First try and file exists, try to delete dir + if retry and err.errno == errno.EEXIST: + print "WARNING: Directory '%s' exists, delete it and retry" % ( + release_tmp_dir + ) + shutil.rmtree(release_tmp_dir) + retry = False + continue + else: + # We already tried to delete or some other error occured + raise + + # Create tarballs + print "Creating release tarballs..." + release_name = release_prefix + '-' + version + print release_prefix, version, release_name + + os.chdir(release_path) + print "- tar" + subprocess.call( + "tar -czf %s.tar.gz %s" % (release_name, release_files), + shell=True + ) + print "- zip" + subprocess.call( + "zip -rq %s.zip %s" % (release_name, release_files), + shell=True + ) + + if cleanup: + print "Deleting working directories" + shutil.rmtree(release_tmp_dir) + if fresh_clone: + shutil.rmtree(repo_path) + else: + print "\nThe following working directories were kept:" + if fresh_clone: + print "- '%s' (repo clone)" % repo_path + print "- '%s' (release temp dir)" % release_tmp_dir + print "Delete them manually when they are no longer needed." + + # Done + print "\nADOdb release %s build complete, files saved in '%s'." % ( + version, + release_path + ) + print "Don't forget to generate a README file with the changelog" + +#end main() + +if __name__ == "__main__": + main() diff --git a/app/vendor/adodb/adodb-php/scripts/updateversion.py b/app/vendor/adodb/adodb-php/scripts/updateversion.py new file mode 100755 index 000000000..0c39fd53b --- /dev/null +++ b/app/vendor/adodb/adodb-php/scripts/updateversion.py @@ -0,0 +1,399 @@ +#!/usr/bin/python -u +''' + ADOdb version update script + + Updates the version number, and release date in all php and html files +''' + +from datetime import date +import getopt +import os +from os import path +import re +import subprocess +import sys + + +# ADOdb version validation regex +# These are used by sed - they are not PCRE ! +_version_dev = "dev" +_version_regex = "[Vv]?([0-9]\.[0-9]+)(\.([0-9]+))?(-?%s)?" % _version_dev +_release_date_regex = "[0-9?]+-.*-[0-9]+" +_changelog_file = "docs/changelog.md" + +_tag_prefix = "v" + + +# Command-line options +options = "hct" +long_options = ["help", "commit", "tag"] + + +def usage(): + print '''Usage: %s version + + Parameters: + version ADOdb version, format: [v]X.YY[a-z|dev] + + Options: + -c | --commit Automatically commit the changes + -t | --tag Create a tag for the new release + -h | --help Show this usage message +''' % ( + path.basename(__file__) + ) +#end usage() + + +def version_is_dev(version): + ''' Returns true if version is a development release + ''' + return version.endswith(_version_dev) + + +def version_is_patch(version): + ''' Returns true if version is a patch release (i.e. X.Y.Z with Z > 0) + ''' + return not version.endswith('.0') + + +def version_parse(version): + ''' Breakdown the version into groups (Z and -dev are optional) + 1:(X.Y), 2:(.Z), 3:(Z), 4:(-dev) + ''' + return re.match(r'^%s$' % _version_regex, version) + + +def version_check(version): + ''' Checks that the given version is valid, exits with error if not. + Returns the SemVer-normalized version without the "v" prefix + - add '.0' if missing patch bit + - add '-' before dev release suffix if needed + ''' + vparse = version_parse(version) + if not vparse: + usage() + print "ERROR: invalid version ! \n" + sys.exit(1) + + vnorm = vparse.group(1) + + # Add .patch version component + if vparse.group(2): + vnorm += vparse.group(2) + else: + # None was specified, assume a .0 release + vnorm += '.0' + + # Normalize version number + if version_is_dev(version): + vnorm += '-' + _version_dev + + return vnorm + + +def get_release_date(version): + ''' Returns the release date in DD-MMM-YYYY format + For development releases, DD-MMM will be ??-??? + ''' + # Development release + if version_is_dev(version): + date_format = "??-???-%Y" + else: + date_format = "%d-%b-%Y" + + # Define release date + return date.today().strftime(date_format) + + +def sed_script(version): + ''' Builds sed script to update version information in source files + ''' + + # Version number and release date + script = r"s/{}\s+(-?)\s+{}/v{} \5 {}/".format( + _version_regex, + _release_date_regex, + version, + get_release_date(version) + ) + + return script + + +def sed_filelist(): + ''' Build list of files to update + ''' + dirlist = [] + for root, dirs, files in os.walk(".", topdown=True): + # Filter files by extensions + files = [ + f for f in files + if re.search(r'\.(php|html?)$', f, re.IGNORECASE) + ] + for fname in files: + dirlist.append(path.join(root, fname)) + + return dirlist + + +def tag_name(version): + return _tag_prefix + version + + +def tag_check(version): + ''' Checks if the tag for the specified version exists in the repository + by attempting to check it out + Throws exception if not + ''' + subprocess.check_call( + "git checkout --quiet " + tag_name(version), + stderr=subprocess.PIPE, + shell=True) + print "Tag '%s' already exists" % tag_name(version) + + +def tag_delete(version): + ''' Deletes the specified tag + ''' + subprocess.check_call( + "git tag --delete " + tag_name(version), + stderr=subprocess.PIPE, + shell=True) + + +def tag_create(version): + ''' Creates the tag for the specified version + Returns True if tag created + ''' + print "Creating release tag '%s'" % tag_name(version) + result = subprocess.call( + "git tag --sign --message '%s' %s" % ( + "ADOdb version %s released %s" % ( + version, + get_release_date(version) + ), + tag_name(version) + ), + shell=True + ) + return result == 0 + + +def section_exists(filename, version, print_message=True): + ''' Checks given file for existing section with specified version + ''' + script = True + for i, line in enumerate(open(filename)): + if re.search(r'^## ' + version, line): + if print_message: + print " Existing section for v%s found," % version, + return True + return False + + +def version_get_previous(version): + ''' Returns the previous version number + Don't decrease major versions (raises exception) + ''' + vprev = version.split('.') + item = len(vprev) - 1 + + while item > 0: + val = int(vprev[item]) + if val > 0: + vprev[item] = str(val - 1) + break + else: + item -= 1 + + if item == 0: + raise ValueError('Refusing to decrease major version number') + + return '.'.join(vprev) + + +def update_changelog(version): + ''' Updates the release date in the Change Log + ''' + print "Updating Changelog" + + vparse = version_parse(version) + + # Version number without '-dev' suffix + version_release = vparse.group(1) + vparse.group(2) + version_previous = version_get_previous(version_release) + + if not section_exists(_changelog_file, version_previous, False): + raise ValueError( + "ERROR: previous version %s does not exist in changelog" % + version_previous + ) + + # Check if version already exists in changelog + version_exists = section_exists(_changelog_file, version_release) + if (not version_exists + and not version_is_patch(version) + and not version_is_dev(version)): + version += '-' + _version_dev + + release_date = get_release_date(version) + + # Development release + # Insert a new section for next release before the most recent one + if version_is_dev(version): + # Check changelog file for existing section + if version_exists: + print "nothing to do" + return + + # No existing section found, insert new one + if version_is_patch(version_release): + print " Inserting new section for hotfix release v%s" % version + else: + print " Inserting new section for v%s" % version_release + # Adjust previous version number (remove patch component) + version_previous = version_parse(version_previous).group(1) + script = "1,/^## {0}/s/^## {0}.*$/## {1} - {2}\\n\\n\\0/".format( + version_previous, + version_release, + release_date + ) + + # Stable release (X.Y.0) + # Replace the 1st occurence of markdown level 2 header matching version + # and release date patterns + elif not version_is_patch(version): + print " Updating release date for v%s" % version + script = r"s/^(## ){0}(\.0)? - {1}.*$/\1{2} - {3}/".format( + vparse.group(1), + _release_date_regex, + version, + release_date + ) + + # Hotfix release (X.Y.[0-9]) + # Insert a new section for the hotfix release before the most recent + # section for version X.Y and display a warning message + else: + if version_exists: + print 'updating release date' + script = "s/^## {0}.*$/## {1} - {2}/".format( + version.replace('.', '\.'), + version, + release_date + ) + else: + print " Inserting new section for hotfix release v%s" % version + script = "1,/^## {0}/s/^## {0}.*$/## {1} - {2}\\n\\n\\0/".format( + version_previous, + version, + release_date + ) + + print " WARNING: review '%s' to ensure added section is correct" % ( + _changelog_file + ) + + subprocess.call( + "sed -r -i '%s' %s " % ( + script, + _changelog_file + ), + shell=True + ) +#end update_changelog + + +def version_set(version, do_commit=True, do_tag=True): + ''' Bump version number and set release date in source files + ''' + print "Preparing version bump commit" + + update_changelog(version) + + print "Updating version and date in source files" + subprocess.call( + "sed -r -i '%s' %s " % ( + sed_script(version), + " ".join(sed_filelist()) + ), + shell=True + ) + print "Version set to %s" % version + + if do_commit: + # Commit changes + print "Committing" + commit_ok = subprocess.call( + "git commit --all --message '%s'" % ( + "Bump version to %s" % version + ), + shell=True + ) + + if do_tag: + tag_ok = tag_create(version) + else: + tag_ok = False + + if commit_ok == 0: + print ''' +NOTE: you should carefully review the new commit, making sure updates +to the files are correct and no additional changes are required. +If everything is fine, then the commit can be pushed upstream; +otherwise: + - Make the required corrections + - Amend the commit ('git commit --all --amend' ) or create a new one''' + + if tag_ok: + print ''' - Drop the tag ('git tag --delete %s') + - run this script again +''' % ( + tag_name(version) + ) + + else: + print "Note: changes have been staged but not committed." +#end version_set() + + +def main(): + # Get command-line options + try: + opts, args = getopt.gnu_getopt(sys.argv[1:], options, long_options) + except getopt.GetoptError, err: + print str(err) + usage() + sys.exit(2) + + if len(args) < 1: + usage() + print "ERROR: please specify the version" + sys.exit(1) + + do_commit = False + do_tag = False + + for opt, val in opts: + if opt in ("-h", "--help"): + usage() + sys.exit(0) + + elif opt in ("-c", "--commit"): + do_commit = True + + elif opt in ("-t", "--tag"): + do_tag = True + + # Mandatory parameters + version = version_check(args[0]) + + # Let's do it + os.chdir(subprocess.check_output('git root', shell=True).rstrip()) + version_set(version, do_commit, do_tag) +#end main() + + +if __name__ == "__main__": + main() diff --git a/app/vendor/adodb/adodb-php/scripts/uploadrelease.py b/app/vendor/adodb/adodb-php/scripts/uploadrelease.py new file mode 100755 index 000000000..5b295cbbb --- /dev/null +++ b/app/vendor/adodb/adodb-php/scripts/uploadrelease.py @@ -0,0 +1,172 @@ +#!/usr/bin/python -u +''' + ADOdb release upload script +''' + +from distutils.version import LooseVersion +import getopt +import glob +import os +from os import path +import re +import subprocess +import sys + + +# Directories and files to exclude from release tarballs +sf_files = "frs.sourceforge.net:/home/frs/project/adodb/" +rsync_cmd = "rsync -vP --rsh ssh {opt} {src} {usr}@{dst}" + +# Command-line options +options = "hn" +long_options = ["help", "dry-run"] + + +def usage(): + print '''Usage: %s [options] username [release_path] + + This script will upload the files in the given directory (or the + current one if unspecified) to Sourceforge. + + Parameters: + username Sourceforge user account + release_path Location of the release files to upload + (see buildrelease.py) + + Options: + -h | --help Show this usage message + -n | --dry-run Do not upload the files +''' % ( + path.basename(__file__) + ) +#end usage() + + +def call_rsync(usr, opt, src, dst): + ''' Calls rsync to upload files with given parameters + usr = ssh username + opt = options + src = source directory + dst = target directory + ''' + global dry_run + + command = rsync_cmd.format(usr=usr, opt=opt, src=src, dst=dst) + + if dry_run: + print command + else: + subprocess.call(command, shell=True) + + +def get_release_version(): + ''' Get the version number from the zip file to upload + ''' + try: + zipfile = glob.glob('adodb-*.zip')[0] + except IndexError: + print "ERROR: release zip file not found in '%s'" % release_path + sys.exit(1) + + try: + version = re.search( + "^adodb-([\d]+\.[\d]+\.[\d]+)\.zip$", + zipfile + ).group(1) + except AttributeError: + print "ERROR: unable to extract version number from '%s'" % zipfile + print " Only 3 groups of digits separated by periods are allowed" + sys.exit(1) + + return version + + +def sourceforge_target_dir(version): + ''' Returns the sourceforge target directory + Base directory as defined in sf_files global variable, plus + - if version >= 5.21: adodb-X.Y + - for older versions: adodb-XYZ-for-php5 + ''' + # Keep only X.Y (discard patch number) + short_version = version.rsplit('.', 1)[0] + + directory = 'adodb-php5-only/' + if LooseVersion(version) >= LooseVersion('5.21'): + directory += "adodb-" + short_version + else: + directory += "adodb-{}-for-php5".format(short_version.replace('.', '')) + + return directory + + +def process_command_line(): + ''' Retrieve command-line options and set global variables accordingly + ''' + global upload_files, upload_doc, dry_run, username, release_path + + # Get command-line options + try: + opts, args = getopt.gnu_getopt(sys.argv[1:], options, long_options) + except getopt.GetoptError, err: + print str(err) + usage() + sys.exit(2) + + if len(args) < 1: + usage() + print "ERROR: please specify the Sourceforge user and release_path" + sys.exit(1) + + # Default values for flags + dry_run = False + + for opt, val in opts: + if opt in ("-h", "--help"): + usage() + sys.exit(0) + + elif opt in ("-n", "--dry-run"): + dry_run = True + + # Mandatory parameters + username = args[0] + + # Change to release directory, current if not specified + try: + release_path = args[1] + os.chdir(release_path) + except IndexError: + release_path = os.getcwd() + + +def upload_release_files(): + ''' Upload release files from source directory to SourceForge + ''' + version = get_release_version() + target = sf_files + sourceforge_target_dir(version) + + print + print "Uploading release files..." + print " Source:", release_path + print " Target: " + target + print + call_rsync( + username, + "", + path.join(release_path, "*"), + target + ) + + +def main(): + process_command_line() + + # Start upload process + print "ADOdb release upload script" + + upload_release_files() + +#end main() + +if __name__ == "__main__": + main() diff --git a/app/vendor/adodb/adodb-php/server.php b/app/vendor/adodb/adodb-php/server.php new file mode 100644 index 000000000..c61287e73 --- /dev/null +++ b/app/vendor/adodb/adodb-php/server.php @@ -0,0 +1,100 @@ +Connect($host,$uid,$pwd,$database)) err($conn->ErrorNo(). $sep . $conn->ErrorMsg()); +$sql = undomq($_REQUEST['sql']); + +if (isset($_REQUEST['fetch'])) + $ADODB_FETCH_MODE = $_REQUEST['fetch']; + +if (isset($_REQUEST['nrows'])) { + $nrows = $_REQUEST['nrows']; + $offset = isset($_REQUEST['offset']) ? $_REQUEST['offset'] : -1; + $rs = $conn->SelectLimit($sql,$nrows,$offset); +} else + $rs = $conn->Execute($sql); +if ($rs){ + //$rs->timeToLive = 1; + echo _rs2serialize($rs,$conn,$sql); + $rs->Close(); +} else + err($conn->ErrorNo(). $sep .$conn->ErrorMsg()); diff --git a/app/vendor/adodb/adodb-php/session/adodb-compress-bzip2.php b/app/vendor/adodb/adodb-php/session/adodb-compress-bzip2.php new file mode 100644 index 000000000..4e6ab501a --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/adodb-compress-bzip2.php @@ -0,0 +1,118 @@ +_block_size; + } + + /** + */ + function setBlockSize($block_size) { + assert('$block_size >= 1'); + assert('$block_size <= 9'); + $this->_block_size = (int) $block_size; + } + + /** + */ + function getWorkLevel() { + return $this->_work_level; + } + + /** + */ + function setWorkLevel($work_level) { + assert('$work_level >= 0'); + assert('$work_level <= 250'); + $this->_work_level = (int) $work_level; + } + + /** + */ + function getMinLength() { + return $this->_min_length; + } + + /** + */ + function setMinLength($min_length) { + assert('$min_length >= 0'); + $this->_min_length = (int) $min_length; + } + + /** + */ + function __construct($block_size = null, $work_level = null, $min_length = null) { + if (!is_null($block_size)) { + $this->setBlockSize($block_size); + } + + if (!is_null($work_level)) { + $this->setWorkLevel($work_level); + } + + if (!is_null($min_length)) { + $this->setMinLength($min_length); + } + } + + /** + */ + function write($data, $key) { + if (strlen($data) < $this->_min_length) { + return $data; + } + + if (!is_null($this->_block_size)) { + if (!is_null($this->_work_level)) { + return bzcompress($data, $this->_block_size, $this->_work_level); + } else { + return bzcompress($data, $this->_block_size); + } + } + + return bzcompress($data); + } + + /** + */ + function read($data, $key) { + return $data ? bzdecompress($data) : $data; + } + +} + +return 1; diff --git a/app/vendor/adodb/adodb-php/session/adodb-compress-gzip.php b/app/vendor/adodb/adodb-php/session/adodb-compress-gzip.php new file mode 100644 index 000000000..53bb0530a --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/adodb-compress-gzip.php @@ -0,0 +1,93 @@ +_level; + } + + /** + */ + function setLevel($level) { + assert('$level >= 0'); + assert('$level <= 9'); + $this->_level = (int) $level; + } + + /** + */ + function getMinLength() { + return $this->_min_length; + } + + /** + */ + function setMinLength($min_length) { + assert('$min_length >= 0'); + $this->_min_length = (int) $min_length; + } + + /** + */ + function __construct($level = null, $min_length = null) { + if (!is_null($level)) { + $this->setLevel($level); + } + + if (!is_null($min_length)) { + $this->setMinLength($min_length); + } + } + + /** + */ + function write($data, $key) { + if (strlen($data) < $this->_min_length) { + return $data; + } + + if (!is_null($this->_level)) { + return gzcompress($data, $this->_level); + } else { + return gzcompress($data); + } + } + + /** + */ + function read($data, $key) { + return $data ? gzuncompress($data) : $data; + } + +} + +return 1; diff --git a/app/vendor/adodb/adodb-php/session/adodb-cryptsession.php b/app/vendor/adodb/adodb-php/session/adodb-cryptsession.php new file mode 100644 index 000000000..763cb50e8 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/adodb-cryptsession.php @@ -0,0 +1,27 @@ +_cipher; + } + + /** + */ + function setCipher($cipher) { + $this->_cipher = $cipher; + } + + /** + */ + function getMode() { + return $this->_mode; + } + + /** + */ + function setMode($mode) { + $this->_mode = $mode; + } + + /** + */ + function getSource() { + return $this->_source; + } + + /** + */ + function setSource($source) { + $this->_source = $source; + } + + /** + */ + function __construct($cipher = null, $mode = null, $source = null) { + if (!$cipher) { + $cipher = MCRYPT_RIJNDAEL_256; + } + if (!$mode) { + $mode = MCRYPT_MODE_ECB; + } + if (!$source) { + $source = MCRYPT_RAND; + } + + $this->_cipher = $cipher; + $this->_mode = $mode; + $this->_source = $source; + } + + /** + */ + function write($data, $key) { + $iv_size = mcrypt_get_iv_size($this->_cipher, $this->_mode); + $iv = mcrypt_create_iv($iv_size, $this->_source); + return mcrypt_encrypt($this->_cipher, $key, $data, $this->_mode, $iv); + } + + /** + */ + function read($data, $key) { + $iv_size = mcrypt_get_iv_size($this->_cipher, $this->_mode); + $iv = mcrypt_create_iv($iv_size, $this->_source); + $rv = mcrypt_decrypt($this->_cipher, $key, $data, $this->_mode, $iv); + return rtrim($rv, "\0"); + } + +} + +return 1; diff --git a/app/vendor/adodb/adodb-php/session/adodb-encrypt-md5.php b/app/vendor/adodb/adodb-php/session/adodb-encrypt-md5.php new file mode 100644 index 000000000..f5b2da616 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/adodb-encrypt-md5.php @@ -0,0 +1,39 @@ +encrypt($data, $key); + } + + /** + */ + function read($data, $key) { + $md5crypt = new MD5Crypt(); + return $md5crypt->decrypt($data, $key); + } + +} + +return 1; diff --git a/app/vendor/adodb/adodb-php/session/adodb-encrypt-secret.php b/app/vendor/adodb/adodb-php/session/adodb-encrypt-secret.php new file mode 100644 index 000000000..96ae85494 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/adodb-encrypt-secret.php @@ -0,0 +1,48 @@ +encrypt($data, $key); + + } + + + function read($data, $key) + { + $sha1crypt = new SHA1Crypt(); + return $sha1crypt->decrypt($data, $key); + + } +} + + + +return 1; diff --git a/app/vendor/adodb/adodb-php/session/adodb-sess.txt b/app/vendor/adodb/adodb-php/session/adodb-sess.txt new file mode 100644 index 000000000..c6c768586 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/adodb-sess.txt @@ -0,0 +1,131 @@ +John, + +I have been an extremely satisfied ADODB user for several years now. + +To give you something back for all your hard work, I've spent the last 3 +days rewriting the adodb-session.php code. + +---------- +What's New +---------- + +Here's a list of the new code's benefits: + +* Combines the functionality of the three files: + +adodb-session.php +adodb-session-clob.php +adodb-cryptsession.php + +each with very similar functionality, into a single file adodb-session.php. +This will ease maintenance and support issues. + +* Supports multiple encryption and compression schemes. + Currently, we support: + + MD5Crypt (crypt.inc.php) + MCrypt + Secure (Horde's emulation of MCrypt, if MCrypt module is not available.) + GZip + BZip2 + +These can be stacked, so if you want to compress and then encrypt your +session data, it's easy. +Also, the built-in MCrypt functions will be *much* faster, and more secure, +than the MD5Crypt code. + +* adodb-session.php contains a single class ADODB_Session that encapsulates +all functionality. + This eliminates the use of global vars and defines (though they are +supported for backwards compatibility). + +* All user defined parameters are now static functions in the ADODB_Session +class. + +New parameters include: + +* encryptionKey(): Define the encryption key used to encrypt the session. +Originally, it was a hard coded string. + +* persist(): Define if the database will be opened in persistent mode. +Originally, the user had to call adodb_sess_open(). + +* dataFieldName(): Define the field name used to store the session data, as +'DATA' appears to be a reserved word in the following cases: + ANSI SQL + IBM DB2 + MS SQL Server + Postgres + SAP + +* filter(): Used to support multiple, simulataneous encryption/compression +schemes. + +* Debug support is improved thru _rsdump() function, which is called after +every database call. + +------------ +What's Fixed +------------ + +The new code includes several bug fixes and enhancements: + +* sesskey is compared in BINARY mode for MySQL, to avoid problems with +session keys that differ only by case. + Of course, the user should define the sesskey field as BINARY, to +correctly fix this problem, otherwise performance will suffer. + +* In ADODB_Session::gc(), if $expire_notify is true, the multiple DELETES in +the original code have been optimized to a single DELETE. + +* In ADODB_Session::destroy(), since "SELECT expireref, sesskey FROM $table +WHERE sesskey = $qkey" will only return a single value, we don't loop on the +result, we simply process the row, if any. + +* We close $rs after every use. + +--------------- +What's the Same +--------------- + +I know backwards compatibility is *very* important to you. Therefore, the +new code is 100% backwards compatible. + +If you like my code, but don't "trust" it's backwards compatible, maybe we +offer it as beta code, in a new directory for a release or two? + +------------ +What's To Do +------------ + +I've vascillated over whether to use a single function to get/set +parameters: + +$user = ADODB_Session::user(); // get +ADODB_Session::user($user); // set + +or to use separate functions (which is the PEAR/Java way): + +$user = ADODB_Session::getUser(); +ADODB_Session::setUser($user); + +I've chosen the former as it's makes for a simpler API, and reduces the +amount of code, but I'd be happy to change it to the latter. + +Also, do you think the class should be a singleton class, versus a static +class? + +Let me know if you find this code useful, and will be including it in the +next release of ADODB. + +If so, I will modify the current documentation to detail the new +functionality. To that end, what file(s) contain the documentation? Please +send them to me if they are not publically available. + +Also, if there is *anything* in the code that you like to see changed, let +me know. + +Thanks, + +Ross + diff --git a/app/vendor/adodb/adodb-php/session/adodb-session-clob.php b/app/vendor/adodb/adodb-php/session/adodb-session-clob.php new file mode 100644 index 000000000..a6906f601 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/adodb-session-clob.php @@ -0,0 +1,24 @@ +Execute('UPDATE '. ADODB_Session::table(). ' SET sesskey='. $conn->qstr($new_id). ' WHERE sesskey='.$conn->qstr($old_id)); + + /* it is possible that the update statement fails due to a collision */ + if (!$ok) { + session_id($old_id); + if (empty($ck)) $ck = session_get_cookie_params(); + setcookie(session_name(), session_id(), false, $ck['path'], $ck['domain'], $ck['secure']); + return false; + } + + return true; +} + +/* + Generate database table for session data + @see http://phplens.com/lens/lensforum/msgs.php?id=12280 + @return 0 if failure, 1 if errors, 2 if successful. + @author Markus Staab http://www.public-4u.de +*/ +function adodb_session_create_table($schemaFile=null,$conn = null) +{ + // set default values + if ($schemaFile===null) $schemaFile = ADODB_SESSION . '/session_schema.xml'; + if ($conn===null) $conn = ADODB_Session::_conn(); + + if (!$conn) return 0; + + $schema = new adoSchema($conn); + $schema->ParseSchema($schemaFile); + return $schema->ExecuteSchema(); +} + +/*! + \static +*/ +class ADODB_Session { + ///////////////////// + // getter/setter methods + ///////////////////// + + /* + + function Lock($lock=null) + { + static $_lock = false; + + if (!is_null($lock)) $_lock = $lock; + return $lock; + } + */ + /*! + */ + function driver($driver = null) { + static $_driver = 'mysql'; + static $set = false; + + if (!is_null($driver)) { + $_driver = trim($driver); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_DRIVER'])) { + return $GLOBALS['ADODB_SESSION_DRIVER']; + } + } + + return $_driver; + } + + /*! + */ + function host($host = null) { + static $_host = 'localhost'; + static $set = false; + + if (!is_null($host)) { + $_host = trim($host); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_CONNECT'])) { + return $GLOBALS['ADODB_SESSION_CONNECT']; + } + } + + return $_host; + } + + /*! + */ + function user($user = null) { + static $_user = 'root'; + static $set = false; + + if (!is_null($user)) { + $_user = trim($user); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_USER'])) { + return $GLOBALS['ADODB_SESSION_USER']; + } + } + + return $_user; + } + + /*! + */ + function password($password = null) { + static $_password = ''; + static $set = false; + + if (!is_null($password)) { + $_password = $password; + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_PWD'])) { + return $GLOBALS['ADODB_SESSION_PWD']; + } + } + + return $_password; + } + + /*! + */ + function database($database = null) { + static $_database = 'xphplens_2'; + static $set = false; + + if (!is_null($database)) { + $_database = trim($database); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_DB'])) { + return $GLOBALS['ADODB_SESSION_DB']; + } + } + + return $_database; + } + + /*! + */ + function persist($persist = null) + { + static $_persist = true; + + if (!is_null($persist)) { + $_persist = trim($persist); + } + + return $_persist; + } + + /*! + */ + function lifetime($lifetime = null) { + static $_lifetime; + static $set = false; + + if (!is_null($lifetime)) { + $_lifetime = (int) $lifetime; + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESS_LIFE'])) { + return $GLOBALS['ADODB_SESS_LIFE']; + } + } + if (!$_lifetime) { + $_lifetime = ini_get('session.gc_maxlifetime'); + if ($_lifetime <= 1) { + // bug in PHP 4.0.3 pl 1 -- how about other versions? + //print "

Session Error: PHP.INI setting session.gc_maxlifetimenot set: $lifetime

"; + $_lifetime = 1440; + } + } + + return $_lifetime; + } + + /*! + */ + function debug($debug = null) { + static $_debug = false; + static $set = false; + + if (!is_null($debug)) { + $_debug = (bool) $debug; + + $conn = ADODB_Session::_conn(); + if ($conn) { + $conn->debug = $_debug; + } + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESS_DEBUG'])) { + return $GLOBALS['ADODB_SESS_DEBUG']; + } + } + + return $_debug; + } + + /*! + */ + function expireNotify($expire_notify = null) { + static $_expire_notify; + static $set = false; + + if (!is_null($expire_notify)) { + $_expire_notify = $expire_notify; + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_EXPIRE_NOTIFY'])) { + return $GLOBALS['ADODB_SESSION_EXPIRE_NOTIFY']; + } + } + + return $_expire_notify; + } + + /*! + */ + function table($table = null) { + static $_table = 'sessions'; + static $set = false; + + if (!is_null($table)) { + $_table = trim($table); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_TBL'])) { + return $GLOBALS['ADODB_SESSION_TBL']; + } + } + + return $_table; + } + + /*! + */ + function optimize($optimize = null) { + static $_optimize = false; + static $set = false; + + if (!is_null($optimize)) { + $_optimize = (bool) $optimize; + $set = true; + } elseif (!$set) { + // backwards compatibility + if (defined('ADODB_SESSION_OPTIMIZE')) { + return true; + } + } + + return $_optimize; + } + + /*! + */ + function syncSeconds($sync_seconds = null) { + static $_sync_seconds = 60; + static $set = false; + + if (!is_null($sync_seconds)) { + $_sync_seconds = (int) $sync_seconds; + $set = true; + } elseif (!$set) { + // backwards compatibility + if (defined('ADODB_SESSION_SYNCH_SECS')) { + return ADODB_SESSION_SYNCH_SECS; + } + } + + return $_sync_seconds; + } + + /*! + */ + function clob($clob = null) { + static $_clob = false; + static $set = false; + + if (!is_null($clob)) { + $_clob = strtolower(trim($clob)); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_USE_LOBS'])) { + return $GLOBALS['ADODB_SESSION_USE_LOBS']; + } + } + + return $_clob; + } + + /*! + */ + function dataFieldName($data_field_name = null) { + static $_data_field_name = 'data'; + + if (!is_null($data_field_name)) { + $_data_field_name = trim($data_field_name); + } + + return $_data_field_name; + } + + /*! + */ + function filter($filter = null) { + static $_filter = array(); + + if (!is_null($filter)) { + if (!is_array($filter)) { + $filter = array($filter); + } + $_filter = $filter; + } + + return $_filter; + } + + /*! + */ + function encryptionKey($encryption_key = null) { + static $_encryption_key = 'CRYPTED ADODB SESSIONS ROCK!'; + + if (!is_null($encryption_key)) { + $_encryption_key = $encryption_key; + } + + return $_encryption_key; + } + + ///////////////////// + // private methods + ///////////////////// + + /*! + */ + function _conn($conn=null) { + return $GLOBALS['ADODB_SESS_CONN']; + } + + /*! + */ + function _crc($crc = null) { + static $_crc = false; + + if (!is_null($crc)) { + $_crc = $crc; + } + + return $_crc; + } + + /*! + */ + function _init() { + session_module_name('user'); + session_set_save_handler( + array('ADODB_Session', 'open'), + array('ADODB_Session', 'close'), + array('ADODB_Session', 'read'), + array('ADODB_Session', 'write'), + array('ADODB_Session', 'destroy'), + array('ADODB_Session', 'gc') + ); + } + + + /*! + */ + function _sessionKey() { + // use this function to create the encryption key for crypted sessions + // crypt the used key, ADODB_Session::encryptionKey() as key and session_id() as salt + return crypt(ADODB_Session::encryptionKey(), session_id()); + } + + /*! + */ + function _dumprs($rs) { + $conn = ADODB_Session::_conn(); + $debug = ADODB_Session::debug(); + + if (!$conn) { + return; + } + + if (!$debug) { + return; + } + + if (!$rs) { + echo "
\$rs is null or false
\n"; + return; + } + + //echo "
\nAffected_Rows=",$conn->Affected_Rows(),"
\n"; + + if (!is_object($rs)) { + return; + } + + require_once ADODB_SESSION.'/../tohtml.inc.php'; + rs2html($rs); + } + + ///////////////////// + // public methods + ///////////////////// + + function config($driver, $host, $user, $password, $database=false,$options=false) + { + ADODB_Session::driver($driver); + ADODB_Session::host($host); + ADODB_Session::user($user); + ADODB_Session::password($password); + ADODB_Session::database($database); + + if ($driver == 'oci8' || $driver == 'oci8po') $options['lob'] = 'CLOB'; + + if (isset($options['table'])) ADODB_Session::table($options['table']); + if (isset($options['lob'])) ADODB_Session::clob($options['lob']); + if (isset($options['debug'])) ADODB_Session::debug($options['debug']); + } + + /*! + Create the connection to the database. + + If $conn already exists, reuse that connection + */ + function open($save_path, $session_name, $persist = null) + { + $conn = ADODB_Session::_conn(); + + if ($conn) { + return true; + } + + $database = ADODB_Session::database(); + $debug = ADODB_Session::debug(); + $driver = ADODB_Session::driver(); + $host = ADODB_Session::host(); + $password = ADODB_Session::password(); + $user = ADODB_Session::user(); + + if (!is_null($persist)) { + ADODB_Session::persist($persist); + } else { + $persist = ADODB_Session::persist(); + } + +# these can all be defaulted to in php.ini +# assert('$database'); +# assert('$driver'); +# assert('$host'); + + $conn = ADONewConnection($driver); + + if ($debug) { + $conn->debug = true; +// ADOConnection::outp( " driver=$driver user=$user pwd=$password db=$database "); + } + + if ($persist) { + switch($persist) { + default: + case 'P': $ok = $conn->PConnect($host, $user, $password, $database); break; + case 'C': $ok = $conn->Connect($host, $user, $password, $database); break; + case 'N': $ok = $conn->NConnect($host, $user, $password, $database); break; + } + } else { + $ok = $conn->Connect($host, $user, $password, $database); + } + + if ($ok) $GLOBALS['ADODB_SESS_CONN'] = $conn; + else + ADOConnection::outp('

Session: connection failed

', false); + + + return $ok; + } + + /*! + Close the connection + */ + function close() + { +/* + $conn = ADODB_Session::_conn(); + if ($conn) $conn->Close(); +*/ + return true; + } + + /* + Slurp in the session variables and return the serialized string + */ + function read($key) + { + $conn = ADODB_Session::_conn(); + $data = ADODB_Session::dataFieldName(); + $filter = ADODB_Session::filter(); + $table = ADODB_Session::table(); + + if (!$conn) { + return ''; + } + + //assert('$table'); + + $qkey = $conn->quote($key); + $binary = $conn->dataProvider === 'mysql' ? '/*! BINARY */' : ''; + + $sql = "SELECT $data FROM $table WHERE sesskey = $binary $qkey AND expiry >= " . time(); + /* Lock code does not work as it needs to hold transaction within whole page, and we don't know if + developer has commited elsewhere... :( + */ + #if (ADODB_Session::Lock()) + # $rs = $conn->RowLock($table, "$binary sesskey = $qkey AND expiry >= " . time(), $data); + #else + + $rs = $conn->Execute($sql); + //ADODB_Session::_dumprs($rs); + if ($rs) { + if ($rs->EOF) { + $v = ''; + } else { + $v = reset($rs->fields); + $filter = array_reverse($filter); + foreach ($filter as $f) { + if (is_object($f)) { + $v = $f->read($v, ADODB_Session::_sessionKey()); + } + } + $v = rawurldecode($v); + } + + $rs->Close(); + + ADODB_Session::_crc(strlen($v) . crc32($v)); + return $v; + } + + return ''; + } + + /*! + Write the serialized data to a database. + + If the data has not been modified since the last read(), we do not write. + */ + function write($key, $val) + { + global $ADODB_SESSION_READONLY; + + if (!empty($ADODB_SESSION_READONLY)) return; + + $clob = ADODB_Session::clob(); + $conn = ADODB_Session::_conn(); + $crc = ADODB_Session::_crc(); + $data = ADODB_Session::dataFieldName(); + $debug = ADODB_Session::debug(); + $driver = ADODB_Session::driver(); + $expire_notify = ADODB_Session::expireNotify(); + $filter = ADODB_Session::filter(); + $lifetime = ADODB_Session::lifetime(); + $table = ADODB_Session::table(); + + if (!$conn) { + return false; + } + $qkey = $conn->qstr($key); + + //assert('$table'); + + $expiry = time() + $lifetime; + + $binary = $conn->dataProvider === 'mysql' ? '/*! BINARY */' : ''; + + // crc32 optimization since adodb 2.1 + // now we only update expiry date, thx to sebastian thom in adodb 2.32 + if ($crc !== false && $crc == (strlen($val) . crc32($val))) { + if ($debug) { + ADOConnection::outp( '

Session: Only updating date - crc32 not changed

'); + } + + $expirevar = ''; + if ($expire_notify) { + $var = reset($expire_notify); + global $$var; + if (isset($$var)) { + $expirevar = $$var; + } + } + + + $sql = "UPDATE $table SET expiry = ".$conn->Param('0').",expireref=".$conn->Param('1')." WHERE $binary sesskey = ".$conn->Param('2')." AND expiry >= ".$conn->Param('3'); + $rs = $conn->Execute($sql,array($expiry,$expirevar,$key,time())); + return true; + } + $val = rawurlencode($val); + foreach ($filter as $f) { + if (is_object($f)) { + $val = $f->write($val, ADODB_Session::_sessionKey()); + } + } + + $arr = array('sesskey' => $key, 'expiry' => $expiry, $data => $val, 'expireref' => ''); + if ($expire_notify) { + $var = reset($expire_notify); + global $$var; + if (isset($$var)) { + $arr['expireref'] = $$var; + } + } + + if (!$clob) { // no lobs, simply use replace() + $arr[$data] = $val; + $rs = $conn->Replace($table, $arr, 'sesskey', $autoQuote = true); + + } else { + // what value shall we insert/update for lob row? + switch ($driver) { + // empty_clob or empty_lob for oracle dbs + case 'oracle': + case 'oci8': + case 'oci8po': + case 'oci805': + $lob_value = sprintf('empty_%s()', strtolower($clob)); + break; + + // null for all other + default: + $lob_value = 'null'; + break; + } + + $conn->StartTrans(); + $expiryref = $conn->qstr($arr['expireref']); + // do we insert or update? => as for sesskey + $rs = $conn->Execute("SELECT COUNT(*) AS cnt FROM $table WHERE $binary sesskey = $qkey"); + if ($rs && reset($rs->fields) > 0) { + $sql = "UPDATE $table SET expiry = $expiry, $data = $lob_value, expireref=$expiryref WHERE sesskey = $qkey"; + } else { + $sql = "INSERT INTO $table (expiry, $data, sesskey,expireref) VALUES ($expiry, $lob_value, $qkey,$expiryref)"; + } + if ($rs)$rs->Close(); + + + $err = ''; + $rs1 = $conn->Execute($sql); + if (!$rs1) $err = $conn->ErrorMsg()."\n"; + + $rs2 = $conn->UpdateBlob($table, $data, $val, " sesskey=$qkey", strtoupper($clob)); + if (!$rs2) $err .= $conn->ErrorMsg()."\n"; + + $rs = ($rs && $rs2) ? true : false; + $conn->CompleteTrans(); + } + + if (!$rs) { + ADOConnection::outp('

Session Replace: ' . $conn->ErrorMsg() . '

', false); + return false; + } else { + // bug in access driver (could be odbc?) means that info is not committed + // properly unless select statement executed in Win2000 + if ($conn->databaseType == 'access') { + $sql = "SELECT sesskey FROM $table WHERE $binary sesskey = $qkey"; + $rs = $conn->Execute($sql); + ADODB_Session::_dumprs($rs); + if ($rs) { + $rs->Close(); + } + } + }/* + if (ADODB_Session::Lock()) { + $conn->CommitTrans(); + }*/ + return $rs ? true : false; + } + + /*! + */ + function destroy($key) { + $conn = ADODB_Session::_conn(); + $table = ADODB_Session::table(); + $expire_notify = ADODB_Session::expireNotify(); + + if (!$conn) { + return false; + } + + //assert('$table'); + + $qkey = $conn->quote($key); + $binary = $conn->dataProvider === 'mysql' ? '/*! BINARY */' : ''; + + if ($expire_notify) { + reset($expire_notify); + $fn = next($expire_notify); + $savem = $conn->SetFetchMode(ADODB_FETCH_NUM); + $sql = "SELECT expireref, sesskey FROM $table WHERE $binary sesskey = $qkey"; + $rs = $conn->Execute($sql); + ADODB_Session::_dumprs($rs); + $conn->SetFetchMode($savem); + if (!$rs) { + return false; + } + if (!$rs->EOF) { + $ref = $rs->fields[0]; + $key = $rs->fields[1]; + //assert('$ref'); + //assert('$key'); + $fn($ref, $key); + } + $rs->Close(); + } + + $sql = "DELETE FROM $table WHERE $binary sesskey = $qkey"; + $rs = $conn->Execute($sql); + ADODB_Session::_dumprs($rs); + + return $rs ? true : false; + } + + /*! + */ + function gc($maxlifetime) + { + $conn = ADODB_Session::_conn(); + $debug = ADODB_Session::debug(); + $expire_notify = ADODB_Session::expireNotify(); + $optimize = ADODB_Session::optimize(); + $sync_seconds = ADODB_Session::syncSeconds(); + $table = ADODB_Session::table(); + + if (!$conn) { + return false; + } + + + $time = time(); + $binary = $conn->dataProvider === 'mysql' ? '/*! BINARY */' : ''; + + if ($expire_notify) { + reset($expire_notify); + $fn = next($expire_notify); + $savem = $conn->SetFetchMode(ADODB_FETCH_NUM); + $sql = "SELECT expireref, sesskey FROM $table WHERE expiry < $time"; + $rs = $conn->Execute($sql); + ADODB_Session::_dumprs($rs); + $conn->SetFetchMode($savem); + if ($rs) { + $conn->StartTrans(); + $keys = array(); + while (!$rs->EOF) { + $ref = $rs->fields[0]; + $key = $rs->fields[1]; + $fn($ref, $key); + $del = $conn->Execute("DELETE FROM $table WHERE sesskey=".$conn->Param('0'),array($key)); + $rs->MoveNext(); + } + $rs->Close(); + + $conn->CompleteTrans(); + } + } else { + + if (1) { + $sql = "SELECT sesskey FROM $table WHERE expiry < $time"; + $arr = $conn->GetAll($sql); + foreach ($arr as $row) { + $sql2 = "DELETE FROM $table WHERE sesskey=".$conn->Param('0'); + $conn->Execute($sql2,array(reset($row))); + } + } else { + $sql = "DELETE FROM $table WHERE expiry < $time"; + $rs = $conn->Execute($sql); + ADODB_Session::_dumprs($rs); + if ($rs) $rs->Close(); + } + if ($debug) { + ADOConnection::outp("

Garbage Collection: $sql

"); + } + } + + // suggested by Cameron, "GaM3R" + if ($optimize) { + $driver = ADODB_Session::driver(); + + if (preg_match('/mysql/i', $driver)) { + $sql = "OPTIMIZE TABLE $table"; + } + if (preg_match('/postgres/i', $driver)) { + $sql = "VACUUM $table"; + } + if (!empty($sql)) { + $conn->Execute($sql); + } + } + + if ($sync_seconds) { + $sql = 'SELECT '; + if ($conn->dataProvider === 'oci8') { + $sql .= "TO_CHAR({$conn->sysTimeStamp}, 'RRRR-MM-DD HH24:MI:SS')"; + } else { + $sql .= $conn->sysTimeStamp; + } + $sql .= " FROM $table"; + + $rs = $conn->SelectLimit($sql, 1); + if ($rs && !$rs->EOF) { + $dbts = reset($rs->fields); + $rs->Close(); + $dbt = $conn->UnixTimeStamp($dbts); + $t = time(); + + if (abs($dbt - $t) >= $sync_seconds) { + $msg = __FILE__ . + ": Server time for webserver {$_SERVER['HTTP_HOST']} not in synch with database: " . + " database=$dbt ($dbts), webserver=$t (diff=". (abs($dbt - $t) / 60) . ' minutes)'; + error_log($msg); + if ($debug) { + ADOConnection::outp("

$msg

"); + } + } + } + } + + return true; + } +} + +ADODB_Session::_init(); +if (empty($ADODB_SESSION_READONLY)) + register_shutdown_function('session_write_close'); + +// for backwards compatability only +function adodb_sess_open($save_path, $session_name, $persist = true) { + return ADODB_Session::open($save_path, $session_name, $persist); +} + +// for backwards compatability only +function adodb_sess_gc($t) +{ + return ADODB_Session::gc($t); +} diff --git a/app/vendor/adodb/adodb-php/session/adodb-session2.php b/app/vendor/adodb/adodb-php/session/adodb-session2.php new file mode 100644 index 000000000..adeefc621 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/adodb-session2.php @@ -0,0 +1,939 @@ +Execute('UPDATE '. ADODB_Session::table(). ' SET sesskey='. $conn->qstr($new_id). ' WHERE sesskey='.$conn->qstr($old_id)); + + /* it is possible that the update statement fails due to a collision */ + if (!$ok) { + session_id($old_id); + if (empty($ck)) $ck = session_get_cookie_params(); + setcookie(session_name(), session_id(), false, $ck['path'], $ck['domain'], $ck['secure']); + return false; + } + + return true; +} + +/* + Generate database table for session data + @see http://phplens.com/lens/lensforum/msgs.php?id=12280 + @return 0 if failure, 1 if errors, 2 if successful. + @author Markus Staab http://www.public-4u.de +*/ +function adodb_session_create_table($schemaFile=null,$conn = null) +{ + // set default values + if ($schemaFile===null) $schemaFile = ADODB_SESSION . '/session_schema2.xml'; + if ($conn===null) $conn = ADODB_Session::_conn(); + + if (!$conn) return 0; + + $schema = new adoSchema($conn); + $schema->ParseSchema($schemaFile); + return $schema->ExecuteSchema(); +} + +/*! + \static +*/ +class ADODB_Session { + ///////////////////// + // getter/setter methods + ///////////////////// + + /* + + function Lock($lock=null) + { + static $_lock = false; + + if (!is_null($lock)) $_lock = $lock; + return $lock; + } + */ + /*! + */ + static function driver($driver = null) + { + static $_driver = 'mysql'; + static $set = false; + + if (!is_null($driver)) { + $_driver = trim($driver); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_DRIVER'])) { + return $GLOBALS['ADODB_SESSION_DRIVER']; + } + } + + return $_driver; + } + + /*! + */ + static function host($host = null) { + static $_host = 'localhost'; + static $set = false; + + if (!is_null($host)) { + $_host = trim($host); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_CONNECT'])) { + return $GLOBALS['ADODB_SESSION_CONNECT']; + } + } + + return $_host; + } + + /*! + */ + static function user($user = null) + { + static $_user = 'root'; + static $set = false; + + if (!is_null($user)) { + $_user = trim($user); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_USER'])) { + return $GLOBALS['ADODB_SESSION_USER']; + } + } + + return $_user; + } + + /*! + */ + static function password($password = null) + { + static $_password = ''; + static $set = false; + + if (!is_null($password)) { + $_password = $password; + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_PWD'])) { + return $GLOBALS['ADODB_SESSION_PWD']; + } + } + + return $_password; + } + + /*! + */ + static function database($database = null) + { + static $_database = ''; + static $set = false; + + if (!is_null($database)) { + $_database = trim($database); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_DB'])) { + return $GLOBALS['ADODB_SESSION_DB']; + } + } + return $_database; + } + + /*! + */ + static function persist($persist = null) + { + static $_persist = true; + + if (!is_null($persist)) { + $_persist = trim($persist); + } + + return $_persist; + } + + /*! + */ + static function lifetime($lifetime = null) + { + static $_lifetime; + static $set = false; + + if (!is_null($lifetime)) { + $_lifetime = (int) $lifetime; + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESS_LIFE'])) { + return $GLOBALS['ADODB_SESS_LIFE']; + } + } + if (!$_lifetime) { + $_lifetime = ini_get('session.gc_maxlifetime'); + if ($_lifetime <= 1) { + // bug in PHP 4.0.3 pl 1 -- how about other versions? + //print "

Session Error: PHP.INI setting session.gc_maxlifetimenot set: $lifetime

"; + $_lifetime = 1440; + } + } + + return $_lifetime; + } + + /*! + */ + static function debug($debug = null) + { + static $_debug = false; + static $set = false; + + if (!is_null($debug)) { + $_debug = (bool) $debug; + + $conn = ADODB_Session::_conn(); + if ($conn) { + #$conn->debug = $_debug; + } + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESS_DEBUG'])) { + return $GLOBALS['ADODB_SESS_DEBUG']; + } + } + + return $_debug; + } + + /*! + */ + static function expireNotify($expire_notify = null) + { + static $_expire_notify; + static $set = false; + + if (!is_null($expire_notify)) { + $_expire_notify = $expire_notify; + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_EXPIRE_NOTIFY'])) { + return $GLOBALS['ADODB_SESSION_EXPIRE_NOTIFY']; + } + } + + return $_expire_notify; + } + + /*! + */ + static function table($table = null) + { + static $_table = 'sessions2'; + static $set = false; + + if (!is_null($table)) { + $_table = trim($table); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_TBL'])) { + return $GLOBALS['ADODB_SESSION_TBL']; + } + } + + return $_table; + } + + /*! + */ + static function optimize($optimize = null) + { + static $_optimize = false; + static $set = false; + + if (!is_null($optimize)) { + $_optimize = (bool) $optimize; + $set = true; + } elseif (!$set) { + // backwards compatibility + if (defined('ADODB_SESSION_OPTIMIZE')) { + return true; + } + } + + return $_optimize; + } + + /*! + */ + static function syncSeconds($sync_seconds = null) { + //echo ("

WARNING: ADODB_SESSION::syncSeconds is longer used, please remove this function for your code

"); + + return 0; + } + + /*! + */ + static function clob($clob = null) { + static $_clob = false; + static $set = false; + + if (!is_null($clob)) { + $_clob = strtolower(trim($clob)); + $set = true; + } elseif (!$set) { + // backwards compatibility + if (isset($GLOBALS['ADODB_SESSION_USE_LOBS'])) { + return $GLOBALS['ADODB_SESSION_USE_LOBS']; + } + } + + return $_clob; + } + + /*! + */ + static function dataFieldName($data_field_name = null) { + //echo ("

WARNING: ADODB_SESSION::dataFieldName() is longer used, please remove this function for your code

"); + return ''; + } + + /*! + */ + static function filter($filter = null) { + static $_filter = array(); + + if (!is_null($filter)) { + if (!is_array($filter)) { + $filter = array($filter); + } + $_filter = $filter; + } + + return $_filter; + } + + /*! + */ + static function encryptionKey($encryption_key = null) { + static $_encryption_key = 'CRYPTED ADODB SESSIONS ROCK!'; + + if (!is_null($encryption_key)) { + $_encryption_key = $encryption_key; + } + + return $_encryption_key; + } + + ///////////////////// + // private methods + ///////////////////// + + /*! + */ + static function _conn($conn=null) { + return isset($GLOBALS['ADODB_SESS_CONN']) ? $GLOBALS['ADODB_SESS_CONN'] : false; + } + + /*! + */ + static function _crc($crc = null) { + static $_crc = false; + + if (!is_null($crc)) { + $_crc = $crc; + } + + return $_crc; + } + + /*! + */ + static function _init() { + session_module_name('user'); + session_set_save_handler( + array('ADODB_Session', 'open'), + array('ADODB_Session', 'close'), + array('ADODB_Session', 'read'), + array('ADODB_Session', 'write'), + array('ADODB_Session', 'destroy'), + array('ADODB_Session', 'gc') + ); + } + + + /*! + */ + static function _sessionKey() { + // use this function to create the encryption key for crypted sessions + // crypt the used key, ADODB_Session::encryptionKey() as key and session_id() as salt + return crypt(ADODB_Session::encryptionKey(), session_id()); + } + + /*! + */ + static function _dumprs(&$rs) { + $conn = ADODB_Session::_conn(); + $debug = ADODB_Session::debug(); + + if (!$conn) { + return; + } + + if (!$debug) { + return; + } + + if (!$rs) { + echo "
\$rs is null or false
\n"; + return; + } + + //echo "
\nAffected_Rows=",$conn->Affected_Rows(),"
\n"; + + if (!is_object($rs)) { + return; + } + $rs = $conn->_rs2rs($rs); + + require_once ADODB_SESSION.'/../tohtml.inc.php'; + rs2html($rs); + $rs->MoveFirst(); + } + + ///////////////////// + // public methods + ///////////////////// + + static function config($driver, $host, $user, $password, $database=false,$options=false) + { + ADODB_Session::driver($driver); + ADODB_Session::host($host); + ADODB_Session::user($user); + ADODB_Session::password($password); + ADODB_Session::database($database); + + if (strncmp($driver, 'oci8', 4) == 0) $options['lob'] = 'CLOB'; + + if (isset($options['table'])) ADODB_Session::table($options['table']); + if (isset($options['lob'])) ADODB_Session::clob($options['lob']); + if (isset($options['debug'])) ADODB_Session::debug($options['debug']); + } + + /*! + Create the connection to the database. + + If $conn already exists, reuse that connection + */ + static function open($save_path, $session_name, $persist = null) + { + $conn = ADODB_Session::_conn(); + + if ($conn) { + return true; + } + + $database = ADODB_Session::database(); + $debug = ADODB_Session::debug(); + $driver = ADODB_Session::driver(); + $host = ADODB_Session::host(); + $password = ADODB_Session::password(); + $user = ADODB_Session::user(); + + if (!is_null($persist)) { + ADODB_Session::persist($persist); + } else { + $persist = ADODB_Session::persist(); + } + +# these can all be defaulted to in php.ini +# assert('$database'); +# assert('$driver'); +# assert('$host'); + + $conn = ADONewConnection($driver); + + if ($debug) { + $conn->debug = true; + ADOConnection::outp( " driver=$driver user=$user db=$database "); + } + + if (empty($conn->_connectionID)) { // not dsn + if ($persist) { + switch($persist) { + default: + case 'P': $ok = $conn->PConnect($host, $user, $password, $database); break; + case 'C': $ok = $conn->Connect($host, $user, $password, $database); break; + case 'N': $ok = $conn->NConnect($host, $user, $password, $database); break; + } + } else { + $ok = $conn->Connect($host, $user, $password, $database); + } + } else { + $ok = true; // $conn->_connectionID is set after call to ADONewConnection + } + + if ($ok) $GLOBALS['ADODB_SESS_CONN'] = $conn; + else + ADOConnection::outp('

Session: connection failed

', false); + + + return $ok; + } + + /*! + Close the connection + */ + static function close() + { +/* + $conn = ADODB_Session::_conn(); + if ($conn) $conn->Close(); +*/ + return true; + } + + /* + Slurp in the session variables and return the serialized string + */ + static function read($key) + { + $conn = ADODB_Session::_conn(); + $filter = ADODB_Session::filter(); + $table = ADODB_Session::table(); + + if (!$conn) { + return ''; + } + + //assert('$table'); + + $binary = $conn->dataProvider === 'mysql' ? '/*! BINARY */' : ''; + + global $ADODB_SESSION_SELECT_FIELDS; + if (!isset($ADODB_SESSION_SELECT_FIELDS)) $ADODB_SESSION_SELECT_FIELDS = 'sessdata'; + $sql = "SELECT $ADODB_SESSION_SELECT_FIELDS FROM $table WHERE sesskey = $binary ".$conn->Param(0)." AND expiry >= " . $conn->sysTimeStamp; + + /* Lock code does not work as it needs to hold transaction within whole page, and we don't know if + developer has commited elsewhere... :( + */ + #if (ADODB_Session::Lock()) + # $rs = $conn->RowLock($table, "$binary sesskey = $qkey AND expiry >= " . time(), sessdata); + #else + $rs = $conn->Execute($sql, array($key)); + //ADODB_Session::_dumprs($rs); + if ($rs) { + if ($rs->EOF) { + $v = ''; + } else { + $v = reset($rs->fields); + $filter = array_reverse($filter); + foreach ($filter as $f) { + if (is_object($f)) { + $v = $f->read($v, ADODB_Session::_sessionKey()); + } + } + $v = rawurldecode($v); + } + + $rs->Close(); + + ADODB_Session::_crc(strlen($v) . crc32($v)); + return $v; + } + + return ''; + } + + /*! + Write the serialized data to a database. + + If the data has not been modified since the last read(), we do not write. + */ + static function write($key, $oval) + { + global $ADODB_SESSION_READONLY; + + if (!empty($ADODB_SESSION_READONLY)) return; + + $clob = ADODB_Session::clob(); + $conn = ADODB_Session::_conn(); + $crc = ADODB_Session::_crc(); + $debug = ADODB_Session::debug(); + $driver = ADODB_Session::driver(); + $expire_notify = ADODB_Session::expireNotify(); + $filter = ADODB_Session::filter(); + $lifetime = ADODB_Session::lifetime(); + $table = ADODB_Session::table(); + + if (!$conn) { + return false; + } + if ($debug) $conn->debug = 1; + $sysTimeStamp = $conn->sysTimeStamp; + + //assert('$table'); + + $expiry = $conn->OffsetDate($lifetime/(24*3600),$sysTimeStamp); + + $binary = $conn->dataProvider === 'mysql' ? '/*! BINARY */' : ''; + + // crc32 optimization since adodb 2.1 + // now we only update expiry date, thx to sebastian thom in adodb 2.32 + if ($crc !== '00' && $crc !== false && $crc == (strlen($oval) . crc32($oval))) { + if ($debug) { + echo '

Session: Only updating date - crc32 not changed

'; + } + + $expirevar = ''; + if ($expire_notify) { + $var = reset($expire_notify); + global $$var; + if (isset($$var)) { + $expirevar = $$var; + } + } + + + $sql = "UPDATE $table SET expiry = $expiry ,expireref=".$conn->Param('0').", modified = $sysTimeStamp WHERE $binary sesskey = ".$conn->Param('1')." AND expiry >= $sysTimeStamp"; + $rs = $conn->Execute($sql,array($expirevar,$key)); + return true; + } + $val = rawurlencode($oval); + foreach ($filter as $f) { + if (is_object($f)) { + $val = $f->write($val, ADODB_Session::_sessionKey()); + } + } + + $expireref = ''; + if ($expire_notify) { + $var = reset($expire_notify); + global $$var; + if (isset($$var)) { + $expireref = $$var; + } + } + + if (!$clob) { // no lobs, simply use replace() + $rs = $conn->Execute("SELECT COUNT(*) AS cnt FROM $table WHERE $binary sesskey = ".$conn->Param(0),array($key)); + if ($rs) $rs->Close(); + + if ($rs && reset($rs->fields) > 0) { + $sql = "UPDATE $table SET expiry=$expiry, sessdata=".$conn->Param(0).", expireref= ".$conn->Param(1).",modified=$sysTimeStamp WHERE sesskey = ".$conn->Param(2); + + } else { + $sql = "INSERT INTO $table (expiry, sessdata, expireref, sesskey, created, modified) + VALUES ($expiry,".$conn->Param('0').", ". $conn->Param('1').", ".$conn->Param('2').", $sysTimeStamp, $sysTimeStamp)"; + } + + + $rs = $conn->Execute($sql,array($val,$expireref,$key)); + + } else { + // what value shall we insert/update for lob row? + if (strncmp($driver, 'oci8', 4) == 0) $lob_value = sprintf('empty_%s()', strtolower($clob)); + else $lob_value = 'null'; + + $conn->StartTrans(); + + $rs = $conn->Execute("SELECT COUNT(*) AS cnt FROM $table WHERE $binary sesskey = ".$conn->Param(0),array($key)); + + if ($rs && reset($rs->fields) > 0) { + $sql = "UPDATE $table SET expiry=$expiry, sessdata=$lob_value, expireref= ".$conn->Param(0).",modified=$sysTimeStamp WHERE sesskey = ".$conn->Param('1'); + + } else { + $sql = "INSERT INTO $table (expiry, sessdata, expireref, sesskey, created, modified) + VALUES ($expiry,$lob_value, ". $conn->Param('0').", ".$conn->Param('1').", $sysTimeStamp, $sysTimeStamp)"; + } + + $rs = $conn->Execute($sql,array($expireref,$key)); + + $qkey = $conn->qstr($key); + $rs2 = $conn->UpdateBlob($table, 'sessdata', $val, " sesskey=$qkey", strtoupper($clob)); + if ($debug) echo "
",htmlspecialchars($oval), "
"; + $rs = @$conn->CompleteTrans(); + + + } + + if (!$rs) { + ADOConnection::outp('

Session Replace: ' . $conn->ErrorMsg() . '

', false); + return false; + } else { + // bug in access driver (could be odbc?) means that info is not committed + // properly unless select statement executed in Win2000 + if ($conn->databaseType == 'access') { + $sql = "SELECT sesskey FROM $table WHERE $binary sesskey = $qkey"; + $rs = $conn->Execute($sql); + ADODB_Session::_dumprs($rs); + if ($rs) { + $rs->Close(); + } + } + }/* + if (ADODB_Session::Lock()) { + $conn->CommitTrans(); + }*/ + return $rs ? true : false; + } + + /*! + */ + static function destroy($key) { + $conn = ADODB_Session::_conn(); + $table = ADODB_Session::table(); + $expire_notify = ADODB_Session::expireNotify(); + + if (!$conn) { + return false; + } + $debug = ADODB_Session::debug(); + if ($debug) $conn->debug = 1; + //assert('$table'); + + $qkey = $conn->quote($key); + $binary = $conn->dataProvider === 'mysql' ? '/*! BINARY */' : ''; + + if ($expire_notify) { + reset($expire_notify); + $fn = next($expire_notify); + $savem = $conn->SetFetchMode(ADODB_FETCH_NUM); + $sql = "SELECT expireref, sesskey FROM $table WHERE $binary sesskey = $qkey"; + $rs = $conn->Execute($sql); + ADODB_Session::_dumprs($rs); + $conn->SetFetchMode($savem); + if (!$rs) { + return false; + } + if (!$rs->EOF) { + $ref = $rs->fields[0]; + $key = $rs->fields[1]; + //assert('$ref'); + //assert('$key'); + $fn($ref, $key); + } + $rs->Close(); + } + + $sql = "DELETE FROM $table WHERE $binary sesskey = $qkey"; + $rs = $conn->Execute($sql); + if ($rs) { + $rs->Close(); + } + + return $rs ? true : false; + } + + /*! + */ + static function gc($maxlifetime) + { + $conn = ADODB_Session::_conn(); + $debug = ADODB_Session::debug(); + $expire_notify = ADODB_Session::expireNotify(); + $optimize = ADODB_Session::optimize(); + $table = ADODB_Session::table(); + + if (!$conn) { + return false; + } + + + $debug = ADODB_Session::debug(); + if ($debug) { + $conn->debug = 1; + $COMMITNUM = 2; + } else { + $COMMITNUM = 20; + } + + //assert('$table'); + + $time = $conn->OffsetDate(-$maxlifetime/24/3600,$conn->sysTimeStamp); + $binary = $conn->dataProvider === 'mysql' ? '/*! BINARY */' : ''; + + if ($expire_notify) { + reset($expire_notify); + $fn = next($expire_notify); + } else { + $fn = false; + } + + $savem = $conn->SetFetchMode(ADODB_FETCH_NUM); + $sql = "SELECT expireref, sesskey FROM $table WHERE expiry < $time ORDER BY 2"; # add order by to prevent deadlock + $rs = $conn->SelectLimit($sql,1000); + if ($debug) ADODB_Session::_dumprs($rs); + $conn->SetFetchMode($savem); + if ($rs) { + $tr = $conn->hasTransactions; + if ($tr) $conn->BeginTrans(); + $keys = array(); + $ccnt = 0; + while (!$rs->EOF) { + $ref = $rs->fields[0]; + $key = $rs->fields[1]; + if ($fn) $fn($ref, $key); + $del = $conn->Execute("DELETE FROM $table WHERE sesskey=".$conn->Param('0'),array($key)); + $rs->MoveNext(); + $ccnt += 1; + if ($tr && $ccnt % $COMMITNUM == 0) { + if ($debug) echo "Commit
\n"; + $conn->CommitTrans(); + $conn->BeginTrans(); + } + } + $rs->Close(); + + if ($tr) $conn->CommitTrans(); + } + + + // suggested by Cameron, "GaM3R" + if ($optimize) { + $driver = ADODB_Session::driver(); + + if (preg_match('/mysql/i', $driver)) { + $sql = "OPTIMIZE TABLE $table"; + } + if (preg_match('/postgres/i', $driver)) { + $sql = "VACUUM $table"; + } + if (!empty($sql)) { + $conn->Execute($sql); + } + } + + + return true; + } +} + +ADODB_Session::_init(); +if (empty($ADODB_SESSION_READONLY)) + register_shutdown_function('session_write_close'); + +// for backwards compatability only +function adodb_sess_open($save_path, $session_name, $persist = true) { + return ADODB_Session::open($save_path, $session_name, $persist); +} + +// for backwards compatability only +function adodb_sess_gc($t) +{ + return ADODB_Session::gc($t); +} diff --git a/app/vendor/adodb/adodb-php/session/adodb-sessions.mysql.sql b/app/vendor/adodb/adodb-php/session/adodb-sessions.mysql.sql new file mode 100644 index 000000000..f90de4493 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/adodb-sessions.mysql.sql @@ -0,0 +1,16 @@ +-- $CVSHeader$ + +CREATE DATABASE /*! IF NOT EXISTS */ adodb_sessions; + +USE adodb_sessions; + +DROP TABLE /*! IF EXISTS */ sessions; + +CREATE TABLE /*! IF NOT EXISTS */ sessions ( + sesskey CHAR(32) /*! BINARY */ NOT NULL DEFAULT '', + expiry INT(11) /*! UNSIGNED */ NOT NULL DEFAULT 0, + expireref VARCHAR(64) DEFAULT '', + data LONGTEXT DEFAULT '', + PRIMARY KEY (sesskey), + INDEX expiry (expiry) +); diff --git a/app/vendor/adodb/adodb-php/session/adodb-sessions.oracle.clob.sql b/app/vendor/adodb/adodb-php/session/adodb-sessions.oracle.clob.sql new file mode 100644 index 000000000..c5c4f2d07 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/adodb-sessions.oracle.clob.sql @@ -0,0 +1,15 @@ +-- $CVSHeader$ + +DROP TABLE adodb_sessions; + +CREATE TABLE sessions ( + sesskey CHAR(32) DEFAULT '' NOT NULL, + expiry INT DEFAULT 0 NOT NULL, + expireref VARCHAR(64) DEFAULT '', + data CLOB DEFAULT '', + PRIMARY KEY (sesskey) +); + +CREATE INDEX ix_expiry ON sessions (expiry); + +QUIT; diff --git a/app/vendor/adodb/adodb-php/session/adodb-sessions.oracle.sql b/app/vendor/adodb/adodb-php/session/adodb-sessions.oracle.sql new file mode 100644 index 000000000..8fd5a3423 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/adodb-sessions.oracle.sql @@ -0,0 +1,16 @@ +-- $CVSHeader$ + +DROP TABLE adodb_sessions; + +CREATE TABLE sessions ( + sesskey CHAR(32) DEFAULT '' NOT NULL, + expiry INT DEFAULT 0 NOT NULL, + expireref VARCHAR(64) DEFAULT '', + data VARCHAR(4000) DEFAULT '', + PRIMARY KEY (sesskey), + INDEX expiry (expiry) +); + +CREATE INDEX ix_expiry ON sessions (expiry); + +QUIT; diff --git a/app/vendor/adodb/adodb-php/session/crypt.inc.php b/app/vendor/adodb/adodb-php/session/crypt.inc.php new file mode 100644 index 000000000..1468cb1ae --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/crypt.inc.php @@ -0,0 +1,157 @@ + +class MD5Crypt{ + function keyED($txt,$encrypt_key) + { + $encrypt_key = md5($encrypt_key); + $ctr=0; + $tmp = ""; + for ($i=0;$ikeyED($tmp,$key)); + } + + function Decrypt($txt,$key) + { + $txt = $this->keyED(base64_decode($txt),$key); + $tmp = ""; + for ($i=0;$i= 58 && $randnumber <= 64) || ($randnumber >= 91 && $randnumber <= 96)) + { + $randnumber = rand(48,120); + } + + $randomPassword .= chr($randnumber); + } + return $randomPassword; + } + +} + + +class SHA1Crypt{ + function keyED($txt,$encrypt_key) + { + + $encrypt_key = sha1($encrypt_key); + $ctr=0; + $tmp = ""; + + for ($i=0;$ikeyED($tmp,$key)); + + } + + + + function Decrypt($txt,$key) + { + + $txt = $this->keyED(base64_decode($txt),$key); + + $tmp = ""; + + for ($i=0;$i= 58 && $randnumber <= 64) || ($randnumber >= 91 && $randnumber <= 96)) + { + $randnumber = rand(48,120); + } + + $randomPassword .= chr($randnumber); + } + + return $randomPassword; + + } + + + +} diff --git a/app/vendor/adodb/adodb-php/session/old/adodb-cryptsession.php b/app/vendor/adodb/adodb-php/session/old/adodb-cryptsession.php new file mode 100644 index 000000000..ca0d79305 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/old/adodb-cryptsession.php @@ -0,0 +1,325 @@ + + + Set tabs to 4 for best viewing. + + Latest version of ADODB is available at http://php.weblogs.com/adodb + ====================================================================== + + This file provides PHP4 session management using the ADODB database +wrapper library. + + Example + ======= + + include('adodb.inc.php'); + #---------------------------------# + include('adodb-cryptsession.php'); + #---------------------------------# + session_start(); + session_register('AVAR'); + $_SESSION['AVAR'] += 1; + print " +-- \$_SESSION['AVAR']={$_SESSION['AVAR']}

"; + + + Installation + ============ + 1. Create a new database in MySQL or Access "sessions" like +so: + + create table sessions ( + SESSKEY char(32) not null, + EXPIRY int(11) unsigned not null, + EXPIREREF varchar(64), + DATA CLOB, + primary key (sesskey) + ); + + 2. Then define the following parameters. You can either modify + this file, or define them before this file is included: + + $ADODB_SESSION_DRIVER='database driver, eg. mysql or ibase'; + $ADODB_SESSION_CONNECT='server to connect to'; + $ADODB_SESSION_USER ='user'; + $ADODB_SESSION_PWD ='password'; + $ADODB_SESSION_DB ='database'; + $ADODB_SESSION_TBL = 'sessions' + + 3. Recommended is PHP 4.0.2 or later. There are documented +session bugs in earlier versions of PHP. + +*/ + + +include_once('crypt.inc.php'); + +if (!defined('_ADODB_LAYER')) { + include (dirname(__FILE__).'/adodb.inc.php'); +} + + /* if database time and system time is difference is greater than this, then give warning */ + define('ADODB_SESSION_SYNCH_SECS',60); + +if (!defined('ADODB_SESSION')) { + + define('ADODB_SESSION',1); + +GLOBAL $ADODB_SESSION_CONNECT, + $ADODB_SESSION_DRIVER, + $ADODB_SESSION_USER, + $ADODB_SESSION_PWD, + $ADODB_SESSION_DB, + $ADODB_SESS_CONN, + $ADODB_SESS_LIFE, + $ADODB_SESS_DEBUG, + $ADODB_SESS_INSERT, + $ADODB_SESSION_EXPIRE_NOTIFY, + $ADODB_SESSION_TBL; + + //$ADODB_SESS_DEBUG = true; + + /* SET THE FOLLOWING PARAMETERS */ +if (empty($ADODB_SESSION_DRIVER)) { + $ADODB_SESSION_DRIVER='mysql'; + $ADODB_SESSION_CONNECT='localhost'; + $ADODB_SESSION_USER ='root'; + $ADODB_SESSION_PWD =''; + $ADODB_SESSION_DB ='xphplens_2'; +} + +if (empty($ADODB_SESSION_TBL)){ + $ADODB_SESSION_TBL = 'sessions'; +} + +if (empty($ADODB_SESSION_EXPIRE_NOTIFY)) { + $ADODB_SESSION_EXPIRE_NOTIFY = false; +} + +function ADODB_Session_Key() +{ +$ADODB_CRYPT_KEY = 'CRYPTED ADODB SESSIONS ROCK!'; + + /* USE THIS FUNCTION TO CREATE THE ENCRYPTION KEY FOR CRYPTED SESSIONS */ + /* Crypt the used key, $ADODB_CRYPT_KEY as key and session_ID as SALT */ + return crypt($ADODB_CRYPT_KEY, session_ID()); +} + +$ADODB_SESS_LIFE = ini_get('session.gc_maxlifetime'); +if ($ADODB_SESS_LIFE <= 1) { + // bug in PHP 4.0.3 pl 1 -- how about other versions? + //print "

Session Error: PHP.INI setting session.gc_maxlifetimenot set: $ADODB_SESS_LIFE

"; + $ADODB_SESS_LIFE=1440; +} + +function adodb_sess_open($save_path, $session_name) +{ +GLOBAL $ADODB_SESSION_CONNECT, + $ADODB_SESSION_DRIVER, + $ADODB_SESSION_USER, + $ADODB_SESSION_PWD, + $ADODB_SESSION_DB, + $ADODB_SESS_CONN, + $ADODB_SESS_DEBUG; + + $ADODB_SESS_INSERT = false; + + if (isset($ADODB_SESS_CONN)) return true; + + $ADODB_SESS_CONN = ADONewConnection($ADODB_SESSION_DRIVER); + if (!empty($ADODB_SESS_DEBUG)) { + $ADODB_SESS_CONN->debug = true; + print" conn=$ADODB_SESSION_CONNECT user=$ADODB_SESSION_USER pwd=$ADODB_SESSION_PWD db=$ADODB_SESSION_DB "; + } + return $ADODB_SESS_CONN->PConnect($ADODB_SESSION_CONNECT, + $ADODB_SESSION_USER,$ADODB_SESSION_PWD,$ADODB_SESSION_DB); + +} + +function adodb_sess_close() +{ +global $ADODB_SESS_CONN; + + if ($ADODB_SESS_CONN) $ADODB_SESS_CONN->Close(); + return true; +} + +function adodb_sess_read($key) +{ +$Crypt = new MD5Crypt; +global $ADODB_SESS_CONN,$ADODB_SESS_INSERT,$ADODB_SESSION_TBL; + $rs = $ADODB_SESS_CONN->Execute("SELECT data FROM $ADODB_SESSION_TBL WHERE sesskey = '$key' AND expiry >= " . time()); + if ($rs) { + if ($rs->EOF) { + $ADODB_SESS_INSERT = true; + $v = ''; + } else { + // Decrypt session data + $v = rawurldecode($Crypt->Decrypt(reset($rs->fields), ADODB_Session_Key())); + } + $rs->Close(); + return $v; + } + else $ADODB_SESS_INSERT = true; + + return ''; +} + +function adodb_sess_write($key, $val) +{ +$Crypt = new MD5Crypt; + global $ADODB_SESS_INSERT,$ADODB_SESS_CONN, $ADODB_SESS_LIFE, $ADODB_SESSION_TBL,$ADODB_SESSION_EXPIRE_NOTIFY; + + $expiry = time() + $ADODB_SESS_LIFE; + + // encrypt session data.. + $val = $Crypt->Encrypt(rawurlencode($val), ADODB_Session_Key()); + + $arr = array('sesskey' => $key, 'expiry' => $expiry, 'data' => $val); + if ($ADODB_SESSION_EXPIRE_NOTIFY) { + $var = reset($ADODB_SESSION_EXPIRE_NOTIFY); + global $$var; + $arr['expireref'] = $$var; + } + $rs = $ADODB_SESS_CONN->Replace($ADODB_SESSION_TBL, + $arr, + 'sesskey',$autoQuote = true); + + if (!$rs) { + ADOConnection::outp( ' +-- Session Replace: '.$ADODB_SESS_CONN->ErrorMsg().'

',false); + } else { + // bug in access driver (could be odbc?) means that info is not commited + // properly unless select statement executed in Win2000 + + if ($ADODB_SESS_CONN->databaseType == 'access') $rs = $ADODB_SESS_CONN->Execute("select sesskey from $ADODB_SESSION_TBL WHERE sesskey='$key'"); + } + return isset($rs); +} + +function adodb_sess_destroy($key) +{ + global $ADODB_SESS_CONN, $ADODB_SESSION_TBL,$ADODB_SESSION_EXPIRE_NOTIFY; + + if ($ADODB_SESSION_EXPIRE_NOTIFY) { + reset($ADODB_SESSION_EXPIRE_NOTIFY); + $fn = next($ADODB_SESSION_EXPIRE_NOTIFY); + $savem = $ADODB_SESS_CONN->SetFetchMode(ADODB_FETCH_NUM); + $rs = $ADODB_SESS_CONN->Execute("SELECT expireref,sesskey FROM $ADODB_SESSION_TBL WHERE sesskey='$key'"); + $ADODB_SESS_CONN->SetFetchMode($savem); + if ($rs) { + $ADODB_SESS_CONN->BeginTrans(); + while (!$rs->EOF) { + $ref = $rs->fields[0]; + $key = $rs->fields[1]; + $fn($ref,$key); + $del = $ADODB_SESS_CONN->Execute("DELETE FROM $ADODB_SESSION_TBL WHERE sesskey='$key'"); + $rs->MoveNext(); + } + $ADODB_SESS_CONN->CommitTrans(); + } + } else { + $qry = "DELETE FROM $ADODB_SESSION_TBL WHERE sesskey = '$key'"; + $rs = $ADODB_SESS_CONN->Execute($qry); + } + return $rs ? true : false; +} + + +function adodb_sess_gc($maxlifetime) { + global $ADODB_SESS_CONN, $ADODB_SESSION_TBL,$ADODB_SESSION_EXPIRE_NOTIFY,$ADODB_SESS_DEBUG; + + if ($ADODB_SESSION_EXPIRE_NOTIFY) { + reset($ADODB_SESSION_EXPIRE_NOTIFY); + $fn = next($ADODB_SESSION_EXPIRE_NOTIFY); + $savem = $ADODB_SESS_CONN->SetFetchMode(ADODB_FETCH_NUM); + $t = time(); + $rs = $ADODB_SESS_CONN->Execute("SELECT expireref,sesskey FROM $ADODB_SESSION_TBL WHERE expiry < $t"); + $ADODB_SESS_CONN->SetFetchMode($savem); + if ($rs) { + $ADODB_SESS_CONN->BeginTrans(); + while (!$rs->EOF) { + $ref = $rs->fields[0]; + $key = $rs->fields[1]; + $fn($ref,$key); + //$del = $ADODB_SESS_CONN->Execute("DELETE FROM $ADODB_SESSION_TBL WHERE sesskey='$key'"); + $rs->MoveNext(); + } + $rs->Close(); + + $ADODB_SESS_CONN->Execute("DELETE FROM $ADODB_SESSION_TBL WHERE expiry < $t"); + $ADODB_SESS_CONN->CommitTrans(); + } + } else { + $qry = "DELETE FROM $ADODB_SESSION_TBL WHERE expiry < " . time(); + $ADODB_SESS_CONN->Execute($qry); + } + + // suggested by Cameron, "GaM3R" + if (defined('ADODB_SESSION_OPTIMIZE')) + { + global $ADODB_SESSION_DRIVER; + + switch( $ADODB_SESSION_DRIVER ) { + case 'mysql': + case 'mysqlt': + $opt_qry = 'OPTIMIZE TABLE '.$ADODB_SESSION_TBL; + break; + case 'postgresql': + case 'postgresql7': + $opt_qry = 'VACUUM '.$ADODB_SESSION_TBL; + break; + } + } + + if ($ADODB_SESS_CONN->dataProvider === 'oci8') $sql = 'select TO_CHAR('.($ADODB_SESS_CONN->sysTimeStamp).', \'RRRR-MM-DD HH24:MI:SS\') from '. $ADODB_SESSION_TBL; + else $sql = 'select '.$ADODB_SESS_CONN->sysTimeStamp.' from '. $ADODB_SESSION_TBL; + + $rs = $ADODB_SESS_CONN->SelectLimit($sql,1); + if ($rs && !$rs->EOF) { + + $dbts = reset($rs->fields); + $rs->Close(); + $dbt = $ADODB_SESS_CONN->UnixTimeStamp($dbts); + $t = time(); + if (abs($dbt - $t) >= ADODB_SESSION_SYNCH_SECS) { + $msg = + __FILE__.": Server time for webserver {$_SERVER['HTTP_HOST']} not in synch with database: database=$dbt ($dbts), webserver=$t (diff=".(abs($dbt-$t)/3600)." hrs)"; + error_log($msg); + if ($ADODB_SESS_DEBUG) ADOConnection::outp(" +-- $msg

"); + } + } + + return true; +} + +session_module_name('user'); +session_set_save_handler( + "adodb_sess_open", + "adodb_sess_close", + "adodb_sess_read", + "adodb_sess_write", + "adodb_sess_destroy", + "adodb_sess_gc"); +} + +/* TEST SCRIPT -- UNCOMMENT */ +/* +if (0) { + + session_start(); + session_register('AVAR'); + $_SESSION['AVAR'] += 1; + print " +-- \$_SESSION['AVAR']={$_SESSION['AVAR']}

"; +} +*/ diff --git a/app/vendor/adodb/adodb-php/session/old/adodb-session-clob.php b/app/vendor/adodb/adodb-php/session/old/adodb-session-clob.php new file mode 100644 index 000000000..3361ea595 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/old/adodb-session-clob.php @@ -0,0 +1,448 @@ +"; + +To force non-persistent connections, call adodb_session_open first before session_start(): + + include('adodb.inc.php'); + include('adodb-session.php'); + adodb_session_open(false,false,false); + session_start(); + session_register('AVAR'); + $_SESSION['AVAR'] += 1; + print " +-- \$_SESSION['AVAR']={$_SESSION['AVAR']}

"; + + + Installation + ============ + 1. Create this table in your database (syntax might vary depending on your db): + + create table sessions ( + SESSKEY char(32) not null, + EXPIRY int(11) unsigned not null, + EXPIREREF varchar(64), + DATA CLOB, + primary key (sesskey) + ); + + + 2. Then define the following parameters in this file: + $ADODB_SESSION_DRIVER='database driver, eg. mysql or ibase'; + $ADODB_SESSION_CONNECT='server to connect to'; + $ADODB_SESSION_USER ='user'; + $ADODB_SESSION_PWD ='password'; + $ADODB_SESSION_DB ='database'; + $ADODB_SESSION_TBL = 'sessions' + $ADODB_SESSION_USE_LOBS = false; (or, if you wanna use CLOBS (= 'CLOB') or ( = 'BLOB') + + 3. Recommended is PHP 4.1.0 or later. There are documented + session bugs in earlier versions of PHP. + + 4. If you want to receive notifications when a session expires, then + you can tag a session with an EXPIREREF, and before the session + record is deleted, we can call a function that will pass the EXPIREREF + as the first parameter, and the session key as the second parameter. + + To do this, define a notification function, say NotifyFn: + + function NotifyFn($expireref, $sesskey) + { + } + + Then you need to define a global variable $ADODB_SESSION_EXPIRE_NOTIFY. + This is an array with 2 elements, the first being the name of the variable + you would like to store in the EXPIREREF field, and the 2nd is the + notification function's name. + + In this example, we want to be notified when a user's session + has expired, so we store the user id in the global variable $USERID, + store this value in the EXPIREREF field: + + $ADODB_SESSION_EXPIRE_NOTIFY = array('USERID','NotifyFn'); + + Then when the NotifyFn is called, we are passed the $USERID as the first + parameter, eg. NotifyFn($userid, $sesskey). +*/ + +if (!defined('_ADODB_LAYER')) { + include (dirname(__FILE__).'/adodb.inc.php'); +} + +if (!defined('ADODB_SESSION')) { + + define('ADODB_SESSION',1); + + /* if database time and system time is difference is greater than this, then give warning */ + define('ADODB_SESSION_SYNCH_SECS',60); + +/****************************************************************************************\ + Global definitions +\****************************************************************************************/ +GLOBAL $ADODB_SESSION_CONNECT, + $ADODB_SESSION_DRIVER, + $ADODB_SESSION_USER, + $ADODB_SESSION_PWD, + $ADODB_SESSION_DB, + $ADODB_SESS_CONN, + $ADODB_SESS_LIFE, + $ADODB_SESS_DEBUG, + $ADODB_SESSION_EXPIRE_NOTIFY, + $ADODB_SESSION_CRC, + $ADODB_SESSION_USE_LOBS, + $ADODB_SESSION_TBL; + + if (!isset($ADODB_SESSION_USE_LOBS)) $ADODB_SESSION_USE_LOBS = 'CLOB'; + + $ADODB_SESS_LIFE = ini_get('session.gc_maxlifetime'); + if ($ADODB_SESS_LIFE <= 1) { + // bug in PHP 4.0.3 pl 1 -- how about other versions? + //print "

Session Error: PHP.INI setting session.gc_maxlifetimenot set: $ADODB_SESS_LIFE

"; + $ADODB_SESS_LIFE=1440; + } + $ADODB_SESSION_CRC = false; + //$ADODB_SESS_DEBUG = true; + + ////////////////////////////////// + /* SET THE FOLLOWING PARAMETERS */ + ////////////////////////////////// + + if (empty($ADODB_SESSION_DRIVER)) { + $ADODB_SESSION_DRIVER='mysql'; + $ADODB_SESSION_CONNECT='localhost'; + $ADODB_SESSION_USER ='root'; + $ADODB_SESSION_PWD =''; + $ADODB_SESSION_DB ='xphplens_2'; + } + + if (empty($ADODB_SESSION_EXPIRE_NOTIFY)) { + $ADODB_SESSION_EXPIRE_NOTIFY = false; + } + // Made table name configurable - by David Johnson djohnson@inpro.net + if (empty($ADODB_SESSION_TBL)){ + $ADODB_SESSION_TBL = 'sessions'; + } + + + // defaulting $ADODB_SESSION_USE_LOBS + if (!isset($ADODB_SESSION_USE_LOBS) || empty($ADODB_SESSION_USE_LOBS)) { + $ADODB_SESSION_USE_LOBS = false; + } + + /* + $ADODB_SESS['driver'] = $ADODB_SESSION_DRIVER; + $ADODB_SESS['connect'] = $ADODB_SESSION_CONNECT; + $ADODB_SESS['user'] = $ADODB_SESSION_USER; + $ADODB_SESS['pwd'] = $ADODB_SESSION_PWD; + $ADODB_SESS['db'] = $ADODB_SESSION_DB; + $ADODB_SESS['life'] = $ADODB_SESS_LIFE; + $ADODB_SESS['debug'] = $ADODB_SESS_DEBUG; + + $ADODB_SESS['debug'] = $ADODB_SESS_DEBUG; + $ADODB_SESS['table'] = $ADODB_SESS_TBL; + */ + +/****************************************************************************************\ + Create the connection to the database. + + If $ADODB_SESS_CONN already exists, reuse that connection +\****************************************************************************************/ +function adodb_sess_open($save_path, $session_name,$persist=true) +{ +GLOBAL $ADODB_SESS_CONN; + if (isset($ADODB_SESS_CONN)) return true; + +GLOBAL $ADODB_SESSION_CONNECT, + $ADODB_SESSION_DRIVER, + $ADODB_SESSION_USER, + $ADODB_SESSION_PWD, + $ADODB_SESSION_DB, + $ADODB_SESS_DEBUG; + + // cannot use & below - do not know why... + $ADODB_SESS_CONN = ADONewConnection($ADODB_SESSION_DRIVER); + if (!empty($ADODB_SESS_DEBUG)) { + $ADODB_SESS_CONN->debug = true; + ADOConnection::outp( " conn=$ADODB_SESSION_CONNECT user=$ADODB_SESSION_USER pwd=$ADODB_SESSION_PWD db=$ADODB_SESSION_DB "); + } + if ($persist) $ok = $ADODB_SESS_CONN->PConnect($ADODB_SESSION_CONNECT, + $ADODB_SESSION_USER,$ADODB_SESSION_PWD,$ADODB_SESSION_DB); + else $ok = $ADODB_SESS_CONN->Connect($ADODB_SESSION_CONNECT, + $ADODB_SESSION_USER,$ADODB_SESSION_PWD,$ADODB_SESSION_DB); + + if (!$ok) ADOConnection::outp( " +-- Session: connection failed

",false); +} + +/****************************************************************************************\ + Close the connection +\****************************************************************************************/ +function adodb_sess_close() +{ +global $ADODB_SESS_CONN; + + if ($ADODB_SESS_CONN) $ADODB_SESS_CONN->Close(); + return true; +} + +/****************************************************************************************\ + Slurp in the session variables and return the serialized string +\****************************************************************************************/ +function adodb_sess_read($key) +{ +global $ADODB_SESS_CONN,$ADODB_SESSION_TBL,$ADODB_SESSION_CRC; + + $rs = $ADODB_SESS_CONN->Execute("SELECT data FROM $ADODB_SESSION_TBL WHERE sesskey = '$key' AND expiry >= " . time()); + if ($rs) { + if ($rs->EOF) { + $v = ''; + } else + $v = rawurldecode(reset($rs->fields)); + + $rs->Close(); + + // new optimization adodb 2.1 + $ADODB_SESSION_CRC = strlen($v).crc32($v); + + return $v; + } + + return ''; // thx to Jorma Tuomainen, webmaster#wizactive.com +} + +/****************************************************************************************\ + Write the serialized data to a database. + + If the data has not been modified since adodb_sess_read(), we do not write. +\****************************************************************************************/ +function adodb_sess_write($key, $val) +{ + global + $ADODB_SESS_CONN, + $ADODB_SESS_LIFE, + $ADODB_SESSION_TBL, + $ADODB_SESS_DEBUG, + $ADODB_SESSION_CRC, + $ADODB_SESSION_EXPIRE_NOTIFY, + $ADODB_SESSION_DRIVER, // added + $ADODB_SESSION_USE_LOBS; // added + + $expiry = time() + $ADODB_SESS_LIFE; + + // crc32 optimization since adodb 2.1 + // now we only update expiry date, thx to sebastian thom in adodb 2.32 + if ($ADODB_SESSION_CRC !== false && $ADODB_SESSION_CRC == strlen($val).crc32($val)) { + if ($ADODB_SESS_DEBUG) echo " +-- Session: Only updating date - crc32 not changed

"; + $qry = "UPDATE $ADODB_SESSION_TBL SET expiry=$expiry WHERE sesskey='$key' AND expiry >= " . time(); + $rs = $ADODB_SESS_CONN->Execute($qry); + return true; + } + $val = rawurlencode($val); + + $arr = array('sesskey' => $key, 'expiry' => $expiry, 'data' => $val); + if ($ADODB_SESSION_EXPIRE_NOTIFY) { + $var = reset($ADODB_SESSION_EXPIRE_NOTIFY); + global $$var; + $arr['expireref'] = $$var; + } + + + if ($ADODB_SESSION_USE_LOBS === false) { // no lobs, simply use replace() + $rs = $ADODB_SESS_CONN->Replace($ADODB_SESSION_TBL,$arr, 'sesskey',$autoQuote = true); + if (!$rs) { + $err = $ADODB_SESS_CONN->ErrorMsg(); + } + } else { + // what value shall we insert/update for lob row? + switch ($ADODB_SESSION_DRIVER) { + // empty_clob or empty_lob for oracle dbs + case "oracle": + case "oci8": + case "oci8po": + case "oci805": + $lob_value = sprintf("empty_%s()", strtolower($ADODB_SESSION_USE_LOBS)); + break; + + // null for all other + default: + $lob_value = "null"; + break; + } + + // do we insert or update? => as for sesskey + $res = $ADODB_SESS_CONN->Execute("select count(*) as cnt from $ADODB_SESSION_TBL where sesskey = '$key'"); + if ($res && reset($res->fields) > 0) { + $qry = sprintf("update %s set expiry = %d, data = %s where sesskey = '%s'", $ADODB_SESSION_TBL, $expiry, $lob_value, $key); + } else { + // insert + $qry = sprintf("insert into %s (sesskey, expiry, data) values ('%s', %d, %s)", $ADODB_SESSION_TBL, $key, $expiry, $lob_value); + } + + $err = ""; + $rs1 = $ADODB_SESS_CONN->Execute($qry); + if (!$rs1) { + $err .= $ADODB_SESS_CONN->ErrorMsg()."\n"; + } + $rs2 = $ADODB_SESS_CONN->UpdateBlob($ADODB_SESSION_TBL, 'data', $val, "sesskey='$key'", strtoupper($ADODB_SESSION_USE_LOBS)); + if (!$rs2) { + $err .= $ADODB_SESS_CONN->ErrorMsg()."\n"; + } + $rs = ($rs1 && $rs2) ? true : false; + } + + if (!$rs) { + ADOConnection::outp( ' +-- Session Replace: '.nl2br($err).'

',false); + } else { + // bug in access driver (could be odbc?) means that info is not commited + // properly unless select statement executed in Win2000 + if ($ADODB_SESS_CONN->databaseType == 'access') + $rs = $ADODB_SESS_CONN->Execute("select sesskey from $ADODB_SESSION_TBL WHERE sesskey='$key'"); + } + return !empty($rs); +} + +function adodb_sess_destroy($key) +{ + global $ADODB_SESS_CONN, $ADODB_SESSION_TBL,$ADODB_SESSION_EXPIRE_NOTIFY; + + if ($ADODB_SESSION_EXPIRE_NOTIFY) { + reset($ADODB_SESSION_EXPIRE_NOTIFY); + $fn = next($ADODB_SESSION_EXPIRE_NOTIFY); + $savem = $ADODB_SESS_CONN->SetFetchMode(ADODB_FETCH_NUM); + $rs = $ADODB_SESS_CONN->Execute("SELECT expireref,sesskey FROM $ADODB_SESSION_TBL WHERE sesskey='$key'"); + $ADODB_SESS_CONN->SetFetchMode($savem); + if ($rs) { + $ADODB_SESS_CONN->BeginTrans(); + while (!$rs->EOF) { + $ref = $rs->fields[0]; + $key = $rs->fields[1]; + $fn($ref,$key); + $del = $ADODB_SESS_CONN->Execute("DELETE FROM $ADODB_SESSION_TBL WHERE sesskey='$key'"); + $rs->MoveNext(); + } + $ADODB_SESS_CONN->CommitTrans(); + } + } else { + $qry = "DELETE FROM $ADODB_SESSION_TBL WHERE sesskey = '$key'"; + $rs = $ADODB_SESS_CONN->Execute($qry); + } + return $rs ? true : false; +} + +function adodb_sess_gc($maxlifetime) +{ + global $ADODB_SESS_DEBUG, $ADODB_SESS_CONN, $ADODB_SESSION_TBL,$ADODB_SESSION_EXPIRE_NOTIFY; + + if ($ADODB_SESSION_EXPIRE_NOTIFY) { + reset($ADODB_SESSION_EXPIRE_NOTIFY); + $fn = next($ADODB_SESSION_EXPIRE_NOTIFY); + $savem = $ADODB_SESS_CONN->SetFetchMode(ADODB_FETCH_NUM); + $t = time(); + $rs = $ADODB_SESS_CONN->Execute("SELECT expireref,sesskey FROM $ADODB_SESSION_TBL WHERE expiry < $t"); + $ADODB_SESS_CONN->SetFetchMode($savem); + if ($rs) { + $ADODB_SESS_CONN->BeginTrans(); + while (!$rs->EOF) { + $ref = $rs->fields[0]; + $key = $rs->fields[1]; + $fn($ref,$key); + $del = $ADODB_SESS_CONN->Execute("DELETE FROM $ADODB_SESSION_TBL WHERE sesskey='$key'"); + $rs->MoveNext(); + } + $rs->Close(); + + //$ADODB_SESS_CONN->Execute("DELETE FROM $ADODB_SESSION_TBL WHERE expiry < $t"); + $ADODB_SESS_CONN->CommitTrans(); + + } + } else { + $ADODB_SESS_CONN->Execute("DELETE FROM $ADODB_SESSION_TBL WHERE expiry < " . time()); + + if ($ADODB_SESS_DEBUG) ADOConnection::outp(" +-- Garbage Collection: $qry

"); + } + // suggested by Cameron, "GaM3R" + if (defined('ADODB_SESSION_OPTIMIZE')) { + global $ADODB_SESSION_DRIVER; + + switch( $ADODB_SESSION_DRIVER ) { + case 'mysql': + case 'mysqlt': + $opt_qry = 'OPTIMIZE TABLE '.$ADODB_SESSION_TBL; + break; + case 'postgresql': + case 'postgresql7': + $opt_qry = 'VACUUM '.$ADODB_SESSION_TBL; + break; + } + if (!empty($opt_qry)) { + $ADODB_SESS_CONN->Execute($opt_qry); + } + } + if ($ADODB_SESS_CONN->dataProvider === 'oci8') $sql = 'select TO_CHAR('.($ADODB_SESS_CONN->sysTimeStamp).', \'RRRR-MM-DD HH24:MI:SS\') from '. $ADODB_SESSION_TBL; + else $sql = 'select '.$ADODB_SESS_CONN->sysTimeStamp.' from '. $ADODB_SESSION_TBL; + + $rs = $ADODB_SESS_CONN->SelectLimit($sql,1); + if ($rs && !$rs->EOF) { + + $dbts = reset($rs->fields); + $rs->Close(); + $dbt = $ADODB_SESS_CONN->UnixTimeStamp($dbts); + $t = time(); + if (abs($dbt - $t) >= ADODB_SESSION_SYNCH_SECS) { + $msg = + __FILE__.": Server time for webserver {$_SERVER['HTTP_HOST']} not in synch with database: database=$dbt ($dbts), webserver=$t (diff=".(abs($dbt-$t)/3600)." hrs)"; + error_log($msg); + if ($ADODB_SESS_DEBUG) ADOConnection::outp(" +-- $msg

"); + } + } + + return true; +} + +session_module_name('user'); +session_set_save_handler( + "adodb_sess_open", + "adodb_sess_close", + "adodb_sess_read", + "adodb_sess_write", + "adodb_sess_destroy", + "adodb_sess_gc"); +} + +/* TEST SCRIPT -- UNCOMMENT */ + +if (0) { + + session_start(); + session_register('AVAR'); + $_SESSION['AVAR'] += 1; + ADOConnection::outp( " +-- \$_SESSION['AVAR']={$_SESSION['AVAR']}

",false); +} diff --git a/app/vendor/adodb/adodb-php/session/old/adodb-session.php b/app/vendor/adodb/adodb-php/session/old/adodb-session.php new file mode 100644 index 000000000..5764339ec --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/old/adodb-session.php @@ -0,0 +1,439 @@ +"; + +To force non-persistent connections, call adodb_session_open first before session_start(): + + include('adodb.inc.php'); + include('adodb-session.php'); + adodb_sess_open(false,false,false); + session_start(); + session_register('AVAR'); + $_SESSION['AVAR'] += 1; + print " +-- \$_SESSION['AVAR']={$_SESSION['AVAR']}

"; + + + Installation + ============ + 1. Create this table in your database (syntax might vary depending on your db): + + create table sessions ( + SESSKEY char(32) not null, + EXPIRY int(11) unsigned not null, + EXPIREREF varchar(64), + DATA text not null, + primary key (sesskey) + ); + + For oracle: + create table sessions ( + SESSKEY char(32) not null, + EXPIRY DECIMAL(16) not null, + EXPIREREF varchar(64), + DATA varchar(4000) not null, + primary key (sesskey) + ); + + + 2. Then define the following parameters. You can either modify + this file, or define them before this file is included: + + $ADODB_SESSION_DRIVER='database driver, eg. mysql or ibase'; + $ADODB_SESSION_CONNECT='server to connect to'; + $ADODB_SESSION_USER ='user'; + $ADODB_SESSION_PWD ='password'; + $ADODB_SESSION_DB ='database'; + $ADODB_SESSION_TBL = 'sessions' + + 3. Recommended is PHP 4.1.0 or later. There are documented + session bugs in earlier versions of PHP. + + 4. If you want to receive notifications when a session expires, then + you can tag a session with an EXPIREREF, and before the session + record is deleted, we can call a function that will pass the EXPIREREF + as the first parameter, and the session key as the second parameter. + + To do this, define a notification function, say NotifyFn: + + function NotifyFn($expireref, $sesskey) + { + } + + Then you need to define a global variable $ADODB_SESSION_EXPIRE_NOTIFY. + This is an array with 2 elements, the first being the name of the variable + you would like to store in the EXPIREREF field, and the 2nd is the + notification function's name. + + In this example, we want to be notified when a user's session + has expired, so we store the user id in the global variable $USERID, + store this value in the EXPIREREF field: + + $ADODB_SESSION_EXPIRE_NOTIFY = array('USERID','NotifyFn'); + + Then when the NotifyFn is called, we are passed the $USERID as the first + parameter, eg. NotifyFn($userid, $sesskey). +*/ + +if (!defined('_ADODB_LAYER')) { + include (dirname(__FILE__).'/adodb.inc.php'); +} + +if (!defined('ADODB_SESSION')) { + + define('ADODB_SESSION',1); + + /* if database time and system time is difference is greater than this, then give warning */ + define('ADODB_SESSION_SYNCH_SECS',60); + + /* + Thanks Joe Li. See http://phplens.com/lens/lensforum/msgs.php?id=11487&x=1 +*/ +function adodb_session_regenerate_id() +{ + $conn = ADODB_Session::_conn(); + if (!$conn) return false; + + $old_id = session_id(); + if (function_exists('session_regenerate_id')) { + session_regenerate_id(); + } else { + session_id(md5(uniqid(rand(), true))); + $ck = session_get_cookie_params(); + setcookie(session_name(), session_id(), false, $ck['path'], $ck['domain'], $ck['secure']); + //@session_start(); + } + $new_id = session_id(); + $ok = $conn->Execute('UPDATE '. ADODB_Session::table(). ' SET sesskey='. $conn->qstr($new_id). ' WHERE sesskey='.$conn->qstr($old_id)); + + /* it is possible that the update statement fails due to a collision */ + if (!$ok) { + session_id($old_id); + if (empty($ck)) $ck = session_get_cookie_params(); + setcookie(session_name(), session_id(), false, $ck['path'], $ck['domain'], $ck['secure']); + return false; + } + + return true; +} + +/****************************************************************************************\ + Global definitions +\****************************************************************************************/ +GLOBAL $ADODB_SESSION_CONNECT, + $ADODB_SESSION_DRIVER, + $ADODB_SESSION_USER, + $ADODB_SESSION_PWD, + $ADODB_SESSION_DB, + $ADODB_SESS_CONN, + $ADODB_SESS_LIFE, + $ADODB_SESS_DEBUG, + $ADODB_SESSION_EXPIRE_NOTIFY, + $ADODB_SESSION_CRC, + $ADODB_SESSION_TBL; + + + $ADODB_SESS_LIFE = ini_get('session.gc_maxlifetime'); + if ($ADODB_SESS_LIFE <= 1) { + // bug in PHP 4.0.3 pl 1 -- how about other versions? + //print "

Session Error: PHP.INI setting session.gc_maxlifetimenot set: $ADODB_SESS_LIFE

"; + $ADODB_SESS_LIFE=1440; + } + $ADODB_SESSION_CRC = false; + //$ADODB_SESS_DEBUG = true; + + ////////////////////////////////// + /* SET THE FOLLOWING PARAMETERS */ + ////////////////////////////////// + + if (empty($ADODB_SESSION_DRIVER)) { + $ADODB_SESSION_DRIVER='mysql'; + $ADODB_SESSION_CONNECT='localhost'; + $ADODB_SESSION_USER ='root'; + $ADODB_SESSION_PWD =''; + $ADODB_SESSION_DB ='xphplens_2'; + } + + if (empty($ADODB_SESSION_EXPIRE_NOTIFY)) { + $ADODB_SESSION_EXPIRE_NOTIFY = false; + } + // Made table name configurable - by David Johnson djohnson@inpro.net + if (empty($ADODB_SESSION_TBL)){ + $ADODB_SESSION_TBL = 'sessions'; + } + + /* + $ADODB_SESS['driver'] = $ADODB_SESSION_DRIVER; + $ADODB_SESS['connect'] = $ADODB_SESSION_CONNECT; + $ADODB_SESS['user'] = $ADODB_SESSION_USER; + $ADODB_SESS['pwd'] = $ADODB_SESSION_PWD; + $ADODB_SESS['db'] = $ADODB_SESSION_DB; + $ADODB_SESS['life'] = $ADODB_SESS_LIFE; + $ADODB_SESS['debug'] = $ADODB_SESS_DEBUG; + + $ADODB_SESS['debug'] = $ADODB_SESS_DEBUG; + $ADODB_SESS['table'] = $ADODB_SESS_TBL; + */ + +/****************************************************************************************\ + Create the connection to the database. + + If $ADODB_SESS_CONN already exists, reuse that connection +\****************************************************************************************/ +function adodb_sess_open($save_path, $session_name,$persist=true) +{ +GLOBAL $ADODB_SESS_CONN; + if (isset($ADODB_SESS_CONN)) return true; + +GLOBAL $ADODB_SESSION_CONNECT, + $ADODB_SESSION_DRIVER, + $ADODB_SESSION_USER, + $ADODB_SESSION_PWD, + $ADODB_SESSION_DB, + $ADODB_SESS_DEBUG; + + // cannot use & below - do not know why... + $ADODB_SESS_CONN = ADONewConnection($ADODB_SESSION_DRIVER); + if (!empty($ADODB_SESS_DEBUG)) { + $ADODB_SESS_CONN->debug = true; + ADOConnection::outp( " conn=$ADODB_SESSION_CONNECT user=$ADODB_SESSION_USER pwd=$ADODB_SESSION_PWD db=$ADODB_SESSION_DB "); + } + if ($persist) $ok = $ADODB_SESS_CONN->PConnect($ADODB_SESSION_CONNECT, + $ADODB_SESSION_USER,$ADODB_SESSION_PWD,$ADODB_SESSION_DB); + else $ok = $ADODB_SESS_CONN->Connect($ADODB_SESSION_CONNECT, + $ADODB_SESSION_USER,$ADODB_SESSION_PWD,$ADODB_SESSION_DB); + + if (!$ok) ADOConnection::outp( " +-- Session: connection failed

",false); +} + +/****************************************************************************************\ + Close the connection +\****************************************************************************************/ +function adodb_sess_close() +{ +global $ADODB_SESS_CONN; + + if ($ADODB_SESS_CONN) $ADODB_SESS_CONN->Close(); + return true; +} + +/****************************************************************************************\ + Slurp in the session variables and return the serialized string +\****************************************************************************************/ +function adodb_sess_read($key) +{ +global $ADODB_SESS_CONN,$ADODB_SESSION_TBL,$ADODB_SESSION_CRC; + + $rs = $ADODB_SESS_CONN->Execute("SELECT data FROM $ADODB_SESSION_TBL WHERE sesskey = '$key' AND expiry >= " . time()); + if ($rs) { + if ($rs->EOF) { + $v = ''; + } else + $v = rawurldecode(reset($rs->fields)); + + $rs->Close(); + + // new optimization adodb 2.1 + $ADODB_SESSION_CRC = strlen($v).crc32($v); + + return $v; + } + + return ''; // thx to Jorma Tuomainen, webmaster#wizactive.com +} + +/****************************************************************************************\ + Write the serialized data to a database. + + If the data has not been modified since adodb_sess_read(), we do not write. +\****************************************************************************************/ +function adodb_sess_write($key, $val) +{ + global + $ADODB_SESS_CONN, + $ADODB_SESS_LIFE, + $ADODB_SESSION_TBL, + $ADODB_SESS_DEBUG, + $ADODB_SESSION_CRC, + $ADODB_SESSION_EXPIRE_NOTIFY; + + $expiry = time() + $ADODB_SESS_LIFE; + + // crc32 optimization since adodb 2.1 + // now we only update expiry date, thx to sebastian thom in adodb 2.32 + if ($ADODB_SESSION_CRC !== false && $ADODB_SESSION_CRC == strlen($val).crc32($val)) { + if ($ADODB_SESS_DEBUG) echo " +-- Session: Only updating date - crc32 not changed

"; + $qry = "UPDATE $ADODB_SESSION_TBL SET expiry=$expiry WHERE sesskey='$key' AND expiry >= " . time(); + $rs = $ADODB_SESS_CONN->Execute($qry); + return true; + } + $val = rawurlencode($val); + + $arr = array('sesskey' => $key, 'expiry' => $expiry, 'data' => $val); + if ($ADODB_SESSION_EXPIRE_NOTIFY) { + $var = reset($ADODB_SESSION_EXPIRE_NOTIFY); + global $$var; + $arr['expireref'] = $$var; + } + $rs = $ADODB_SESS_CONN->Replace($ADODB_SESSION_TBL,$arr, + 'sesskey',$autoQuote = true); + + if (!$rs) { + ADOConnection::outp( ' +-- Session Replace: '.$ADODB_SESS_CONN->ErrorMsg().'

',false); + } else { + // bug in access driver (could be odbc?) means that info is not commited + // properly unless select statement executed in Win2000 + if ($ADODB_SESS_CONN->databaseType == 'access') + $rs = $ADODB_SESS_CONN->Execute("select sesskey from $ADODB_SESSION_TBL WHERE sesskey='$key'"); + } + return !empty($rs); +} + +function adodb_sess_destroy($key) +{ + global $ADODB_SESS_CONN, $ADODB_SESSION_TBL,$ADODB_SESSION_EXPIRE_NOTIFY; + + if ($ADODB_SESSION_EXPIRE_NOTIFY) { + reset($ADODB_SESSION_EXPIRE_NOTIFY); + $fn = next($ADODB_SESSION_EXPIRE_NOTIFY); + $savem = $ADODB_SESS_CONN->SetFetchMode(ADODB_FETCH_NUM); + $rs = $ADODB_SESS_CONN->Execute("SELECT expireref,sesskey FROM $ADODB_SESSION_TBL WHERE sesskey='$key'"); + $ADODB_SESS_CONN->SetFetchMode($savem); + if ($rs) { + $ADODB_SESS_CONN->BeginTrans(); + while (!$rs->EOF) { + $ref = $rs->fields[0]; + $key = $rs->fields[1]; + $fn($ref,$key); + $del = $ADODB_SESS_CONN->Execute("DELETE FROM $ADODB_SESSION_TBL WHERE sesskey='$key'"); + $rs->MoveNext(); + } + $ADODB_SESS_CONN->CommitTrans(); + } + } else { + $qry = "DELETE FROM $ADODB_SESSION_TBL WHERE sesskey = '$key'"; + $rs = $ADODB_SESS_CONN->Execute($qry); + } + return $rs ? true : false; +} + +function adodb_sess_gc($maxlifetime) +{ + global $ADODB_SESS_DEBUG, $ADODB_SESS_CONN, $ADODB_SESSION_TBL,$ADODB_SESSION_EXPIRE_NOTIFY; + + if ($ADODB_SESSION_EXPIRE_NOTIFY) { + reset($ADODB_SESSION_EXPIRE_NOTIFY); + $fn = next($ADODB_SESSION_EXPIRE_NOTIFY); + $savem = $ADODB_SESS_CONN->SetFetchMode(ADODB_FETCH_NUM); + $t = time(); + $rs = $ADODB_SESS_CONN->Execute("SELECT expireref,sesskey FROM $ADODB_SESSION_TBL WHERE expiry < $t"); + $ADODB_SESS_CONN->SetFetchMode($savem); + if ($rs) { + $ADODB_SESS_CONN->BeginTrans(); + while (!$rs->EOF) { + $ref = $rs->fields[0]; + $key = $rs->fields[1]; + $fn($ref,$key); + $del = $ADODB_SESS_CONN->Execute("DELETE FROM $ADODB_SESSION_TBL WHERE sesskey='$key'"); + $rs->MoveNext(); + } + $rs->Close(); + + $ADODB_SESS_CONN->CommitTrans(); + + } + } else { + $qry = "DELETE FROM $ADODB_SESSION_TBL WHERE expiry < " . time(); + $ADODB_SESS_CONN->Execute($qry); + + if ($ADODB_SESS_DEBUG) ADOConnection::outp(" +-- Garbage Collection: $qry

"); + } + // suggested by Cameron, "GaM3R" + if (defined('ADODB_SESSION_OPTIMIZE')) { + global $ADODB_SESSION_DRIVER; + + switch( $ADODB_SESSION_DRIVER ) { + case 'mysql': + case 'mysqlt': + $opt_qry = 'OPTIMIZE TABLE '.$ADODB_SESSION_TBL; + break; + case 'postgresql': + case 'postgresql7': + $opt_qry = 'VACUUM '.$ADODB_SESSION_TBL; + break; + } + if (!empty($opt_qry)) { + $ADODB_SESS_CONN->Execute($opt_qry); + } + } + if ($ADODB_SESS_CONN->dataProvider === 'oci8') $sql = 'select TO_CHAR('.($ADODB_SESS_CONN->sysTimeStamp).', \'RRRR-MM-DD HH24:MI:SS\') from '. $ADODB_SESSION_TBL; + else $sql = 'select '.$ADODB_SESS_CONN->sysTimeStamp.' from '. $ADODB_SESSION_TBL; + + $rs = $ADODB_SESS_CONN->SelectLimit($sql,1); + if ($rs && !$rs->EOF) { + + $dbts = reset($rs->fields); + $rs->Close(); + $dbt = $ADODB_SESS_CONN->UnixTimeStamp($dbts); + $t = time(); + + if (abs($dbt - $t) >= ADODB_SESSION_SYNCH_SECS) { + + $msg = + __FILE__.": Server time for webserver {$_SERVER['HTTP_HOST']} not in synch with database: database=$dbt ($dbts), webserver=$t (diff=".(abs($dbt-$t)/3600)." hrs)"; + error_log($msg); + if ($ADODB_SESS_DEBUG) ADOConnection::outp(" +-- $msg

"); + } + } + + return true; +} + +session_module_name('user'); +session_set_save_handler( + "adodb_sess_open", + "adodb_sess_close", + "adodb_sess_read", + "adodb_sess_write", + "adodb_sess_destroy", + "adodb_sess_gc"); +} + +/* TEST SCRIPT -- UNCOMMENT */ + +if (0) { + + session_start(); + session_register('AVAR'); + $_SESSION['AVAR'] += 1; + ADOConnection::outp( " +-- \$_SESSION['AVAR']={$_SESSION['AVAR']}

",false); +} diff --git a/app/vendor/adodb/adodb-php/session/old/crypt.inc.php b/app/vendor/adodb/adodb-php/session/old/crypt.inc.php new file mode 100644 index 000000000..9c347db06 --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/old/crypt.inc.php @@ -0,0 +1,63 @@ + +class MD5Crypt{ + function keyED($txt,$encrypt_key) + { + $encrypt_key = md5($encrypt_key); + $ctr=0; + $tmp = ""; + for ($i=0;$ikeyED($tmp,$key)); + } + + function Decrypt($txt,$key) + { + $txt = $this->keyED(base64_decode($txt),$key); + $tmp = ""; + for ($i=0;$i= 58 && $randnumber <= 64) || ($randnumber >= 91 && $randnumber <= 96)) + { + $randnumber = rand(48,120); + } + + $randomPassword .= chr($randnumber); + } + return $randomPassword; + } + +} diff --git a/app/vendor/adodb/adodb-php/session/session_schema.xml b/app/vendor/adodb/adodb-php/session/session_schema.xml new file mode 100644 index 000000000..27e47bfee --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/session_schema.xml @@ -0,0 +1,26 @@ + + + + table for ADOdb session-management + + + session key + + + + + + + + + + + + + + + + + +
+
diff --git a/app/vendor/adodb/adodb-php/session/session_schema2.xml b/app/vendor/adodb/adodb-php/session/session_schema2.xml new file mode 100644 index 000000000..f0d3ec94a --- /dev/null +++ b/app/vendor/adodb/adodb-php/session/session_schema2.xml @@ -0,0 +1,38 @@ + + + + table for ADOdb session-management + + + session key + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/app/vendor/adodb/adodb-php/tests/benchmark.php b/app/vendor/adodb/adodb-php/tests/benchmark.php new file mode 100644 index 000000000..5ba5e8bcb --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/benchmark.php @@ -0,0 +1,86 @@ + + + + + ADODB Benchmarks + + + +ADODB Version: $ADODB_version Host: $db->host   Database: $db->database"; + + // perform query once to cache results so we are only testing throughput + $rs = $db->Execute($sql); + if (!$rs){ + print "Error in recordset

"; + return; + } + $arr = $rs->GetArray(); + //$db->debug = true; + global $ADODB_COUNTRECS; + $ADODB_COUNTRECS = false; + $start = microtime(); + for ($i=0; $i < $max; $i++) { + $rs = $db->Execute($sql); + $arr = $rs->GetArray(); + // print $arr[0][1]; + } + $end = microtime(); + $start = explode(' ',$start); + $end = explode(' ',$end); + + //print_r($start); + //print_r($end); + + // print_r($arr); + $total = $end[0]+trim($end[1]) - $start[0]-trim($start[1]); + printf ("

seconds = %8.2f for %d iterations each with %d records

",$total,$max, sizeof($arr)); + flush(); + + + //$db->Close(); +} +include("testdatabases.inc.php"); + +?> + + + + diff --git a/app/vendor/adodb/adodb-php/tests/client.php b/app/vendor/adodb/adodb-php/tests/client.php new file mode 100644 index 000000000..5b30065a0 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/client.php @@ -0,0 +1,199 @@ + + +'; + var_dump(parse_url('odbc_mssql://userserver/')); + die(); + +include('../adodb.inc.php'); +include('../tohtml.inc.php'); + + function send2server($url,$sql) + { + $url .= '?sql='.urlencode($sql); + print "

$url

"; + $rs = csv2rs($url,$err); + if ($err) print $err; + return $rs; + } + + function print_pre($s) + { + print "
";print_r($s);print "
"; + } + + +$serverURL = 'http://localhost/php/phplens/adodb/server.php'; +$testhttp = false; + +$sql1 = "insertz into products (productname) values ('testprod 1')"; +$sql2 = "insert into products (productname) values ('testprod 1')"; +$sql3 = "insert into products (productname) values ('testprod 2')"; +$sql4 = "delete from products where productid>80"; +$sql5 = 'select * from products'; + +if ($testhttp) { + print "Client Driver Tests

"; + print "

Test Error

"; + $rs = send2server($serverURL,$sql1); + print_pre($rs); + print "
"; + + print "

Test Insert

"; + + $rs = send2server($serverURL,$sql2); + print_pre($rs); + print "
"; + + print "

Test Insert2

"; + + $rs = send2server($serverURL,$sql3); + print_pre($rs); + print "
"; + + print "

Test Delete

"; + + $rs = send2server($serverURL,$sql4); + print_pre($rs); + print "
"; + + + print "

Test Select

"; + $rs = send2server($serverURL,$sql5); + if ($rs) rs2html($rs); + + print "
"; +} + + +print "

CLIENT Driver Tests

"; +$conn = ADONewConnection('csv'); +$conn->Connect($serverURL); +$conn->debug = true; + +print "

Bad SQL

"; + +$rs = $conn->Execute($sql1); + +print "

Insert SQL 1

"; +$rs = $conn->Execute($sql2); + +print "

Insert SQL 2

"; +$rs = $conn->Execute($sql3); + +print "

Select SQL

"; +$rs = $conn->Execute($sql5); +if ($rs) rs2html($rs); + +print "

Delete SQL

"; +$rs = $conn->Execute($sql4); + +print "

Select SQL

"; +$rs = $conn->Execute($sql5); +if ($rs) rs2html($rs); + + +/* EXPECTED RESULTS FOR HTTP TEST: + +Test Insert +http://localhost/php/adodb/server.php?sql=insert+into+products+%28productname%29+values+%28%27testprod%27%29 + +adorecordset Object +( + [dataProvider] => native + [fields] => + [blobSize] => 64 + [canSeek] => + [EOF] => 1 + [emptyTimeStamp] => + [emptyDate] => + [debug] => + [timeToLive] => 0 + [bind] => + [_numOfRows] => -1 + [_numOfFields] => 0 + [_queryID] => 1 + [_currentRow] => -1 + [_closed] => + [_inited] => + [sql] => insert into products (productname) values ('testprod') + [affectedrows] => 1 + [insertid] => 81 +) + + +-------------------------------------------------------------------------------- + +Test Insert2 +http://localhost/php/adodb/server.php?sql=insert+into+products+%28productname%29+values+%28%27testprod%27%29 + +adorecordset Object +( + [dataProvider] => native + [fields] => + [blobSize] => 64 + [canSeek] => + [EOF] => 1 + [emptyTimeStamp] => + [emptyDate] => + [debug] => + [timeToLive] => 0 + [bind] => + [_numOfRows] => -1 + [_numOfFields] => 0 + [_queryID] => 1 + [_currentRow] => -1 + [_closed] => + [_inited] => + [sql] => insert into products (productname) values ('testprod') + [affectedrows] => 1 + [insertid] => 82 +) + + +-------------------------------------------------------------------------------- + +Test Delete +http://localhost/php/adodb/server.php?sql=delete+from+products+where+productid%3E80 + +adorecordset Object +( + [dataProvider] => native + [fields] => + [blobSize] => 64 + [canSeek] => + [EOF] => 1 + [emptyTimeStamp] => + [emptyDate] => + [debug] => + [timeToLive] => 0 + [bind] => + [_numOfRows] => -1 + [_numOfFields] => 0 + [_queryID] => 1 + [_currentRow] => -1 + [_closed] => + [_inited] => + [sql] => delete from products where productid>80 + [affectedrows] => 2 + [insertid] => 0 +) + +[more stuff deleted] + . + . + . +*/ diff --git a/app/vendor/adodb/adodb-php/tests/pdo.php b/app/vendor/adodb/adodb-php/tests/pdo.php new file mode 100644 index 000000000..31ca59697 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/pdo.php @@ -0,0 +1,92 @@ +"; +try { + echo "New Connection\n"; + + + $dsn = 'pdo_mysql://root:@localhost/northwind?persist'; + + if (!empty($dsn)) { + $DB = NewADOConnection($dsn) || die("CONNECT FAILED"); + $connstr = $dsn; + } else { + + $DB = NewADOConnection('pdo'); + + echo "Connect\n"; + + $u = ''; $p = ''; + /* + $connstr = 'odbc:nwind'; + + $connstr = 'oci:'; + $u = 'scott'; + $p = 'natsoft'; + + + $connstr ="sqlite:d:\inetpub\adodb\sqlite.db"; + */ + + $connstr = "mysql:dbname=northwind"; + $u = 'root'; + + $connstr = "pgsql:dbname=test"; + $u = 'tester'; + $p = 'test'; + + $DB->Connect($connstr,$u,$p) || die("CONNECT FAILED"); + + } + + echo "connection string=$connstr\n Execute\n"; + + //$ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $rs = $DB->Execute("select * from ADOXYZ where id<3"); + if ($DB->ErrorNo()) echo "*** errno=".$DB->ErrorNo() . " ".($DB->ErrorMsg())."\n"; + + + //print_r(get_class_methods($DB->_stmt)); + + if (!$rs) die("NO RS"); + + echo "Meta\n"; + for ($i=0; $i < $rs->NumCols(); $i++) { + var_dump($rs->FetchField($i)); + echo "
"; + } + + echo "FETCH\n"; + $cnt = 0; + while (!$rs->EOF) { + adodb_pr($rs->fields); + $rs->MoveNext(); + if ($cnt++ > 1000) break; + } + + echo "
--------------------------------------------------------
\n\n\n"; + + $stmt = $DB->PrepareStmt("select * from ADOXYZ"); + + $rs = $stmt->Execute(); + $cols = $stmt->NumCols(); // execute required + + echo "COLS = $cols"; + for($i=1;$i<=$cols;$i++) { + $v = $stmt->_stmt->getColumnMeta($i); + var_dump($v); + } + + echo "e=".$stmt->ErrorNo() . " ".($stmt->ErrorMsg())."\n"; + while ($arr = $rs->FetchRow()) { + adodb_pr($arr); + } + die("DONE\n"); + +} catch (exception $e) { + echo "
";
+	echo $e;
+	echo "
"; +} diff --git a/app/vendor/adodb/adodb-php/tests/test-active-record.php b/app/vendor/adodb/adodb-php/tests/test-active-record.php new file mode 100644 index 000000000..59471620c --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test-active-record.php @@ -0,0 +1,140 @@ += 5) { + include('../adodb-exceptions.inc.php'); + echo "

Exceptions included

"; + } + } + + $db = NewADOConnection('mysql://root@localhost/northwind?persist'); + $db->debug=1; + ADOdb_Active_Record::SetDatabaseAdapter($db); + + + $db->Execute("CREATE TEMPORARY TABLE `persons` ( + `id` int(10) unsigned NOT NULL auto_increment, + `name_first` varchar(100) NOT NULL default '', + `name_last` varchar(100) NOT NULL default '', + `favorite_color` varchar(100) NOT NULL default '', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM; + "); + + $db->Execute("CREATE TEMPORARY TABLE `children` ( + `id` int(10) unsigned NOT NULL auto_increment, + `person_id` int(10) unsigned NOT NULL, + `name_first` varchar(100) NOT NULL default '', + `name_last` varchar(100) NOT NULL default '', + `favorite_pet` varchar(100) NOT NULL default '', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM; + "); + + class Person extends ADOdb_Active_Record{ function ret($v) {return $v;} } + $person = new Person(); + ADOdb_Active_Record::$_quoteNames = '111'; + + echo "

Output of getAttributeNames: "; + var_dump($person->getAttributeNames()); + + /** + * Outputs the following: + * array(4) { + * [0]=> + * string(2) "id" + * [1]=> + * string(9) "name_first" + * [2]=> + * string(8) "name_last" + * [3]=> + * string(13) "favorite_color" + * } + */ + + $person = new Person(); + $person->name_first = 'Andi'; + $person->name_last = 'Gutmans'; + $person->save(); // this save() will fail on INSERT as favorite_color is a must fill... + + + $person = new Person(); + $person->name_first = 'Andi'; + $person->name_last = 'Gutmans'; + $person->favorite_color = 'blue'; + $person->save(); // this save will perform an INSERT successfully + + echo "

The Insert ID generated:"; print_r($person->id); + + $person->favorite_color = 'red'; + $person->save(); // this save() will perform an UPDATE + + $person = new Person(); + $person->name_first = 'John'; + $person->name_last = 'Lim'; + $person->favorite_color = 'lavender'; + $person->save(); // this save will perform an INSERT successfully + + // load record where id=2 into a new ADOdb_Active_Record + $person2 = new Person(); + $person2->Load('id=2'); + + $activeArr = $db->GetActiveRecordsClass($class = "Person",$table = "Persons","id=".$db->Param(0),array(2)); + $person2 = $activeArr[0]; + echo "

Name (should be John): ",$person->name_first, "
Class (should be Person): ",get_class($person2),"
"; + + $db->Execute("insert into children (person_id,name_first,name_last) values (2,'Jill','Lim')"); + $db->Execute("insert into children (person_id,name_first,name_last) values (2,'Joan','Lim')"); + $db->Execute("insert into children (person_id,name_first,name_last) values (2,'JAMIE','Lim')"); + + $newperson2 = new Person(); + $person2->HasMany('children','person_id'); + $person2->Load('id=2'); + $person2->name_last='green'; + $c = $person2->children; + $person2->save(); + + if (is_array($c) && sizeof($c) == 3 && $c[0]->name_first=='Jill' && $c[1]->name_first=='Joan' + && $c[2]->name_first == 'JAMIE') echo "OK Loaded HasMany
"; + else { + var_dump($c); + echo "error loading hasMany should have 3 array elements Jill Joan Jamie
"; + } + + class Child extends ADOdb_Active_Record{}; + $ch = new Child('children',array('id')); + $ch->BelongsTo('person','person_id','id'); + $ch->Load('id=1'); + if ($ch->name_first !== 'Jill') echo "error in Loading Child
"; + + $p = $ch->person; + if ($p->name_first != 'John') echo "Error loading belongsTo
"; + else echo "OK loading BelongTo
"; + + $p->hasMany('children','person_id'); + $p->LoadRelations('children', " Name_first like 'J%' order by id",1,2); + if (sizeof($p->children) == 2 && $p->children[1]->name_first == 'JAMIE') echo "OK LoadRelations
"; + else echo "error LoadRelations
"; + + $db->Execute("CREATE TEMPORARY TABLE `persons2` ( + `id` int(10) unsigned NOT NULL auto_increment, + `name_first` varchar(100) NOT NULL default '', + `name_last` varchar(100) NOT NULL default '', + `favorite_color` varchar(100) default '', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM; + "); + + $p = new adodb_active_record('persons2'); + $p->name_first = 'James'; + + $p->name_last = 'James'; + + $p->HasMany('children','person_id'); + $p->children; + var_dump($p); + $p->Save(); diff --git a/app/vendor/adodb/adodb-php/tests/test-active-recs2.php b/app/vendor/adodb/adodb-php/tests/test-active-recs2.php new file mode 100644 index 000000000..f5898fcda --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test-active-recs2.php @@ -0,0 +1,76 @@ +Connect("localhost","tester","test","test"); +} else + $db = NewADOConnection('oci8://scott:natsoft@/'); + + +$arr = $db->ServerInfo(); +echo "

$db->dataProvider: {$arr['description']}

"; + +$arr = $db->GetActiveRecords('products',' productid<10'); +adodb_pr($arr); + +ADOdb_Active_Record::SetDatabaseAdapter($db); +if (!$db) die('failed'); + + + + +$rec = new ADODB_Active_Record('photos'); + +$rec = new ADODB_Active_Record('products'); + + +adodb_pr($rec->getAttributeNames()); + +echo "
"; + + +$rec->load('productid=2'); +adodb_pr($rec); + +$db->debug=1; + + +$rec->productname = 'Changie Chan'.rand(); + +$rec->insert(); +$rec->update(); + +$rec->productname = 'Changie Chan 99'; +$rec->replace(); + + +$rec2 = new ADODB_Active_Record('products'); +$rec->load('productid=3'); +$rec->save(); + +$rec = new ADODB_Active_record('products'); +$rec->productname = 'John ActiveRec'; +$rec->notes = 22; +#$rec->productid=0; +$rec->discontinued=1; +$rec->Save(); +$rec->supplierid=33; +$rec->Save(); +$rec->discontinued=0; +$rec->Save(); +$rec->Delete(); + +echo "

Affected Rows after delete=".$db->Affected_Rows()."

"; diff --git a/app/vendor/adodb/adodb-php/tests/test-active-relations.php b/app/vendor/adodb/adodb-php/tests/test-active-relations.php new file mode 100644 index 000000000..7a98d479f --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test-active-relations.php @@ -0,0 +1,85 @@ +debug=1; + ADOdb_Active_Record::SetDatabaseAdapter($db); + + $db->Execute("CREATE TEMPORARY TABLE `persons` ( + `id` int(10) unsigned NOT NULL auto_increment, + `name_first` varchar(100) NOT NULL default '', + `name_last` varchar(100) NOT NULL default '', + `favorite_color` varchar(100) NOT NULL default '', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM; + "); + + $db->Execute("CREATE TEMPORARY TABLE `children` ( + `id` int(10) unsigned NOT NULL auto_increment, + `person_id` int(10) unsigned NOT NULL, + `name_first` varchar(100) NOT NULL default '', + `name_last` varchar(100) NOT NULL default '', + `favorite_pet` varchar(100) NOT NULL default '', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM; + "); + + + $db->Execute("insert into children (person_id,name_first,name_last) values (1,'Jill','Lim')"); + $db->Execute("insert into children (person_id,name_first,name_last) values (1,'Joan','Lim')"); + $db->Execute("insert into children (person_id,name_first,name_last) values (1,'JAMIE','Lim')"); + + ADODB_Active_Record::TableHasMany('persons', 'children','person_id'); + class person extends ADOdb_Active_Record{} + + $person = new person(); +# $person->HasMany('children','person_id'); ## this is affects all other instances of Person + + $person->name_first = 'John'; + $person->name_last = 'Lim'; + $person->favorite_color = 'lavender'; + $person->save(); // this save will perform an INSERT successfully + + $person2 = new person(); + $person2->Load('id=1'); + + $c = $person2->children; + if (is_array($c) && sizeof($c) == 3 && $c[0]->name_first=='Jill' && $c[1]->name_first=='Joan' + && $c[2]->name_first == 'JAMIE') echo "OK Loaded HasMany
"; + else { + var_dump($c); + echo "error loading hasMany should have 3 array elements Jill Joan Jamie
"; + } + + class child extends ADOdb_Active_Record{}; + ADODB_Active_Record::TableBelongsTo('children','person','person_id','id'); + $ch = new Child('children',array('id')); + + $ch->Load('id=1'); + if ($ch->name_first !== 'Jill') echo "error in Loading Child
"; + + $p = $ch->person; + if (!$p || $p->name_first != 'John') echo "Error loading belongsTo
"; + else echo "OK loading BelongTo
"; + + if ($p) { + #$p->HasMany('children','person_id'); ## this is affects all other instances of Person + $p->LoadRelations('children', 'order by id',1,2); + if (sizeof($p->children) == 2 && $p->children[1]->name_first == 'JAMIE') echo "OK LoadRelations
"; + else { + var_dump($p->children); + echo "error LoadRelations
"; + } + + unset($p->children); + $p->LoadRelations('children', " name_first like 'J%' order by id",1,2); + } + if ($p) + foreach($p->children as $c) { + echo " Saving $c->name_first
"; + $c->name_first .= ' K.'; + $c->Save(); + } diff --git a/app/vendor/adodb/adodb-php/tests/test-active-relationsx.php b/app/vendor/adodb/adodb-php/tests/test-active-relationsx.php new file mode 100644 index 000000000..0f28f728c --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test-active-relationsx.php @@ -0,0 +1,418 @@ +\n", $txt); + echo $txt; + } + + include_once('../adodb.inc.php'); + include_once('../adodb-active-recordx.inc.php'); + + + $db = NewADOConnection('mysql://root@localhost/test'); + $db->debug=0; + ADOdb_Active_Record::SetDatabaseAdapter($db); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("Preparing database using SQL queries (creating 'people', 'children')\n"); + + $db->Execute("DROP TABLE `people`"); + $db->Execute("DROP TABLE `children`"); + $db->Execute("DROP TABLE `artists`"); + $db->Execute("DROP TABLE `songs`"); + + $db->Execute("CREATE TABLE `people` ( + `id` int(10) unsigned NOT NULL auto_increment, + `name_first` varchar(100) NOT NULL default '', + `name_last` varchar(100) NOT NULL default '', + `favorite_color` varchar(100) NOT NULL default '', + PRIMARY KEY (`id`) + ) ENGINE=MyISAM; + "); + $db->Execute("CREATE TABLE `children` ( + `person_id` int(10) unsigned NOT NULL, + `name_first` varchar(100) NOT NULL default '', + `name_last` varchar(100) NOT NULL default '', + `favorite_pet` varchar(100) NOT NULL default '', + `id` int(10) unsigned NOT NULL auto_increment, + PRIMARY KEY (`id`) + ) ENGINE=MyISAM; + "); + + $db->Execute("CREATE TABLE `artists` ( + `name` varchar(100) NOT NULL default '', + `artistuniqueid` int(10) unsigned NOT NULL auto_increment, + PRIMARY KEY (`artistuniqueid`) + ) ENGINE=MyISAM; + "); + + $db->Execute("CREATE TABLE `songs` ( + `name` varchar(100) NOT NULL default '', + `artistid` int(10) NOT NULL, + `recordid` int(10) unsigned NOT NULL auto_increment, + PRIMARY KEY (`recordid`) + ) ENGINE=MyISAM; + "); + + $db->Execute("insert into children (person_id,name_first,name_last,favorite_pet) values (1,'Jill','Lim','tortoise')"); + $db->Execute("insert into children (person_id,name_first,name_last) values (1,'Joan','Lim')"); + $db->Execute("insert into children (person_id,name_first,name_last) values (1,'JAMIE','Lim')"); + + $db->Execute("insert into artists (artistuniqueid, name) values(1,'Elvis Costello')"); + $db->Execute("insert into songs (recordid, name, artistid) values(1,'No Hiding Place', 1)"); + $db->Execute("insert into songs (recordid, name, artistid) values(2,'American Gangster Time', 1)"); + + // This class _implicitely_ relies on the 'people' table (pluralized form of 'person') + class Person extends ADOdb_Active_Record + { + function __construct() + { + parent::__construct(); + $this->hasMany('children'); + } + } + // This class _implicitely_ relies on the 'children' table + class Child extends ADOdb_Active_Record + { + function __construct() + { + parent::__construct(); + $this->belongsTo('person'); + } + } + // This class _explicitely_ relies on the 'children' table and shares its metadata with Child + class Kid extends ADOdb_Active_Record + { + function __construct() + { + parent::__construct('children'); + $this->belongsTo('person'); + } + } + // This class _explicitely_ relies on the 'children' table but does not share its metadata + class Rugrat extends ADOdb_Active_Record + { + function __construct() + { + parent::__construct('children', false, false, array('new' => true)); + } + } + + class Artist extends ADOdb_Active_Record + { + function __construct() + { + parent::__construct('artists', array('artistuniqueid')); + $this->hasMany('songs', 'artistid'); + } + } + class Song extends ADOdb_Active_Record + { + function __construct() + { + parent::__construct('songs', array('recordid')); + $this->belongsTo('artist', 'artistid'); + } + } + + ar_echo("Inserting person in 'people' table ('John Lim, he likes lavender')\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $person = new Person(); + $person->name_first = 'John'; + $person->name_last = 'Lim'; + $person->favorite_color = 'lavender'; + $person->save(); // this save will perform an INSERT successfully + + $person = new Person(); + $person->name_first = 'Lady'; + $person->name_last = 'Cat'; + $person->favorite_color = 'green'; + $person->save(); + + $child = new Child(); + $child->name_first = 'Fluffy'; + $child->name_last = 'Cat'; + $child->favorite_pet = 'Cat Lady'; + $child->person_id = $person->id; + $child->save(); + + $child = new Child(); + $child->name_first = 'Sun'; + $child->name_last = 'Cat'; + $child->favorite_pet = 'Cat Lady'; + $child->person_id = $person->id; + $child->save(); + + $err_count = 0; + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("person->Find('id=1') [Lazy Method]\n"); + ar_echo("person is loaded but its children will be loaded on-demand later on\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $person = new Person(); + $people = $person->Find('id=1'); + ar_echo((ar_assert(found($people, "'name_first' => 'John'"))) ? "[OK] Found John\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(notfound($people, "'favorite_pet' => 'tortoise'"))) ? "[OK] No relation yet\n" : "[!!] Found relation when I shouldn't\n"); + ar_echo("\n-- Lazily Loading Children:\n\n"); + foreach($people as $aperson) + { + foreach($aperson->children as $achild) + { + if($achild->name_first); + } + } + ar_echo((ar_assert(found($people, "'favorite_pet' => 'tortoise'"))) ? "[OK] Found relation: child\n" : "[!!] Missing relation: child\n"); + ar_echo((ar_assert(found($people, "'name_first' => 'Joan'"))) ? "[OK] Found Joan\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($people, "'name_first' => 'JAMIE'"))) ? "[OK] Found JAMIE\n" : "[!!] Find failed\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("person->Find('id=1' ... ADODB_WORK_AR) [Worker Method]\n"); + ar_echo("person is loaded, and so are its children\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $person = new Person(); + $people = $person->Find('id=1', false, false, array('loading' => ADODB_WORK_AR)); + ar_echo((ar_assert(found($people, "'name_first' => 'John'"))) ? "[OK] Found John\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($people, "'favorite_pet' => 'tortoise'"))) ? "[OK] Found relation: child\n" : "[!!] Missing relation: child\n"); + ar_echo((ar_assert(found($people, "'name_first' => 'Joan'"))) ? "[OK] Found Joan\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($people, "'name_first' => 'JAMIE'"))) ? "[OK] Found JAMIE\n" : "[!!] Find failed\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("person->Find('id=1' ... ADODB_JOIN_AR) [Join Method]\n"); + ar_echo("person and its children are loaded using a single query\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $person = new Person(); + // When I specifically ask for a join, I have to specify which table id I am looking up + // otherwise the SQL parser will wonder which table's id that would be. + $people = $person->Find('people.id=1', false, false, array('loading' => ADODB_JOIN_AR)); + ar_echo((ar_assert(found($people, "'name_first' => 'John'"))) ? "[OK] Found John\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($people, "'favorite_pet' => 'tortoise'"))) ? "[OK] Found relation: child\n" : "[!!] Missing relation: child\n"); + ar_echo((ar_assert(found($people, "'name_first' => 'Joan'"))) ? "[OK] Found Joan\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($people, "'name_first' => 'JAMIE'"))) ? "[OK] Found JAMIE\n" : "[!!] Find failed\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("person->Load('people.id=1') [Join Method]\n"); + ar_echo("Load() always uses the join method since it returns only one row\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $person = new Person(); + // Under the hood, Load(), since it returns only one row, always perform a join + // Therefore we need to clarify which id we are talking about. + $person->Load('people.id=1'); + ar_echo((ar_assert(found($person, "'name_first' => 'John'"))) ? "[OK] Found John\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($person, "'favorite_pet' => 'tortoise'"))) ? "[OK] Found relation: child\n" : "[!!] Missing relation: child\n"); + ar_echo((ar_assert(found($person, "'name_first' => 'Joan'"))) ? "[OK] Found Joan\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($person, "'name_first' => 'JAMIE'"))) ? "[OK] Found JAMIE\n" : "[!!] Find failed\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("child->Load('children.id=1') [Join Method]\n"); + ar_echo("We are now loading from the 'children' table, not from 'people'\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $child = new Child(); + $child->Load('children.id=1'); + ar_echo((ar_assert(found($child, "'name_first' => 'Jill'"))) ? "[OK] Found Jill\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($child, "'favorite_color' => 'lavender'"))) ? "[OK] Found relation: person\n" : "[!!] Missing relation: person\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("child->Find('children.id=1' ... ADODB_WORK_AR) [Worker Method]\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $child = new Child(); + $children = $child->Find('id=1', false, false, array('loading' => ADODB_WORK_AR)); + ar_echo((ar_assert(found($children, "'name_first' => 'Jill'"))) ? "[OK] Found Jill\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($children, "'favorite_color' => 'lavender'"))) ? "[OK] Found relation: person\n" : "[!!] Missing relation: person\n"); + ar_echo((ar_assert(notfound($children, "'name_first' => 'Joan'"))) ? "[OK] No Joan relation\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(notfound($children, "'name_first' => 'JAMIE'"))) ? "[OK] No JAMIE relation\n" : "[!!] Find failed\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("kid->Find('children.id=1' ... ADODB_WORK_AR) [Worker Method]\n"); + ar_echo("Where we see that kid shares relationships with child because they are stored\n"); + ar_echo("in the common table's metadata structure.\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $kid = new Kid('children'); + $kids = $kid->Find('children.id=1', false, false, array('loading' => ADODB_WORK_AR)); + ar_echo((ar_assert(found($kids, "'name_first' => 'Jill'"))) ? "[OK] Found Jill\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($kids, "'favorite_color' => 'lavender'"))) ? "[OK] Found relation: person\n" : "[!!] Missing relation: person\n"); + ar_echo((ar_assert(notfound($kids, "'name_first' => 'Joan'"))) ? "[OK] No Joan relation\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(notfound($kids, "'name_first' => 'JAMIE'"))) ? "[OK] No JAMIE relation\n" : "[!!] Find failed\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("kid->Find('children.id=1' ... ADODB_LAZY_AR) [Lazy Method]\n"); + ar_echo("Of course, lazy loading also retrieve medata information...\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $kid = new Kid('children'); + $kids = $kid->Find('children.id=1', false, false, array('loading' => ADODB_LAZY_AR)); + ar_echo((ar_assert(found($kids, "'name_first' => 'Jill'"))) ? "[OK] Found Jill\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(notfound($kids, "'favorite_color' => 'lavender'"))) ? "[OK] No relation yet\n" : "[!!] Found relation when I shouldn't\n"); + ar_echo("\n-- Lazily Loading People:\n\n"); + foreach($kids as $akid) + { + if($akid->person); + } + ar_echo((ar_assert(found($kids, "'favorite_color' => 'lavender'"))) ? "[OK] Found relation: person\n" : "[!!] Missing relation: person\n"); + ar_echo((ar_assert(notfound($kids, "'name_first' => 'Joan'"))) ? "[OK] No Joan relation\n" : "[!!] Found relation when I shouldn't\n"); + ar_echo((ar_assert(notfound($kids, "'name_first' => 'JAMIE'"))) ? "[OK] No JAMIE relation\n" : "[!!] Found relation when I shouldn't\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("rugrat->Find('children.id=1' ... ADODB_WORK_AR) [Worker Method]\n"); + ar_echo("In rugrat's constructor it is specified that\nit must forget any existing relation\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $rugrat = new Rugrat('children'); + $rugrats = $rugrat->Find('children.id=1', false, false, array('loading' => ADODB_WORK_AR)); + ar_echo((ar_assert(found($rugrats, "'name_first' => 'Jill'"))) ? "[OK] Found Jill\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(notfound($rugrats, "'favorite_color' => 'lavender'"))) ? "[OK] No relation found\n" : "[!!] Found relation when I shouldn't\n"); + ar_echo((ar_assert(notfound($rugrats, "'name_first' => 'Joan'"))) ? "[OK] No Joan relation\n" : "[!!] Found relation when I shouldn't\n"); + ar_echo((ar_assert(notfound($rugrats, "'name_first' => 'JAMIE'"))) ? "[OK] No JAMIE relation\n" : "[!!] Found relation when I shouldn't\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("kid->Find('children.id=1' ... ADODB_WORK_AR) [Worker Method]\n"); + ar_echo("Note how only rugrat forgot its relations - kid is fine.\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $kid = new Kid('children'); + $kids = $kid->Find('children.id=1', false, false, array('loading' => ADODB_WORK_AR)); + ar_echo((ar_assert(found($kids, "'name_first' => 'Jill'"))) ? "[OK] Found Jill\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($kids, "'favorite_color' => 'lavender'"))) ? "[OK] I did not forget relation: person\n" : "[!!] I should not have forgotten relation: person\n"); + ar_echo((ar_assert(notfound($kids, "'name_first' => 'Joan'"))) ? "[OK] No Joan relation\n" : "[!!] Found relation when I shouldn't\n"); + ar_echo((ar_assert(notfound($kids, "'name_first' => 'JAMIE'"))) ? "[OK] No JAMIE relation\n" : "[!!] Found relation when I shouldn't\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("rugrat->Find('children.id=1' ... ADODB_WORK_AR) [Worker Method]\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $rugrat = new Rugrat('children'); + $rugrats = $rugrat->Find('children.id=1', false, false, array('loading' => ADODB_WORK_AR)); + $arugrat = $rugrats[0]; + ar_echo((ar_assert(found($arugrat, "'name_first' => 'Jill'"))) ? "[OK] Found Jill\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(notfound($arugrat, "'favorite_color' => 'lavender'"))) ? "[OK] No relation yet\n" : "[!!] Found relation when I shouldn't\n"); + + ar_echo("\n-- Loading relations:\n\n"); + $arugrat->belongsTo('person'); + $arugrat->LoadRelations('person', 'order by id', 0, 2); + ar_echo((ar_assert(found($arugrat, "'favorite_color' => 'lavender'"))) ? "[OK] Found relation: person\n" : "[!!] Missing relation: person\n"); + ar_echo((ar_assert(found($arugrat, "'name_first' => 'Jill'"))) ? "[OK] Found Jill\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(notfound($arugrat, "'name_first' => 'Joan'"))) ? "[OK] No Joan relation\n" : "[!!] Found relation when I shouldn't\n"); + ar_echo((ar_assert(notfound($arugrat, "'name_first' => 'JAMIE'"))) ? "[OK] No Joan relation\n" : "[!!] Found relation when I shouldn't\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("person->Find('1=1') [Lazy Method]\n"); + ar_echo("And now for our finale...\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $person = new Person(); + $people = $person->Find('1=1', false, false, array('loading' => ADODB_LAZY_AR)); + ar_echo((ar_assert(found($people, "'name_first' => 'John'"))) ? "[OK] Found John\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(notfound($people, "'favorite_pet' => 'tortoise'"))) ? "[OK] No relation yet\n" : "[!!] Found relation when I shouldn't\n"); + ar_echo((ar_assert(notfound($people, "'name_first' => 'Fluffy'"))) ? "[OK] No Fluffy yet\n" : "[!!] Found Fluffy relation when I shouldn't\n"); + ar_echo("\n-- Lazily Loading Everybody:\n\n"); + foreach($people as $aperson) + { + foreach($aperson->children as $achild) + { + if($achild->name_first); + } + } + ar_echo((ar_assert(found($people, "'favorite_pet' => 'tortoise'"))) ? "[OK] Found relation: child\n" : "[!!] Missing relation: child\n"); + ar_echo((ar_assert(found($people, "'name_first' => 'Joan'"))) ? "[OK] Found Joan\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($people, "'name_first' => 'JAMIE'"))) ? "[OK] Found JAMIE\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($people, "'name_first' => 'Lady'"))) ? "[OK] Found Cat Lady\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($people, "'name_first' => 'Fluffy'"))) ? "[OK] Found Fluffy\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($people, "'name_first' => 'Sun'"))) ? "[OK] Found Sun\n" : "[!!] Find failed\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("artist->Load('artistuniqueid=1') [Join Method]\n"); + ar_echo("Yes, we are dabbling in the musical field now..\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $artist = new Artist(); + $artist->Load('artistuniqueid=1'); + ar_echo((ar_assert(found($artist, "'name' => 'Elvis Costello'"))) ? "[OK] Found Elvis Costello\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($artist, "'name' => 'No Hiding Place'"))) ? "[OK] Found relation: song\n" : "[!!] Missing relation: song\n"); + + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("song->Load('recordid=1') [Join Method]\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $song = new Song(); + $song->Load('recordid=1'); + ar_echo((ar_assert(found($song, "'name' => 'No Hiding Place'"))) ? "[OK] Found song\n" : "[!!] Find failed\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("artist->Find('artistuniqueid=1' ... ADODB_JOIN_AR) [Join Method]\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $artist = new Artist(); + $artists = $artist->Find('artistuniqueid=1', false, false, array('loading' => ADODB_JOIN_AR)); + ar_echo((ar_assert(found($artists, "'name' => 'Elvis Costello'"))) ? "[OK] Found Elvis Costello\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($artists, "'name' => 'No Hiding Place'"))) ? "[OK] Found relation: song\n" : "[!!] Missing relation: song\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("song->Find('recordid=1' ... ADODB_JOIN_AR) [Join Method]\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $song = new Song(); + $songs = $song->Find('recordid=1', false, false, array('loading' => ADODB_JOIN_AR)); + ar_echo((ar_assert(found($songs, "'name' => 'No Hiding Place'"))) ? "[OK] Found song\n" : "[!!] Find failed\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("artist->Find('artistuniqueid=1' ... ADODB_WORK_AR) [Work Method]\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $artist = new Artist(); + $artists = $artist->Find('artistuniqueid=1', false, false, array('loading' => ADODB_WORK_AR)); + ar_echo((ar_assert(found($artists, "'name' => 'Elvis Costello'"))) ? "[OK] Found Elvis Costello\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(found($artists, "'name' => 'No Hiding Place'"))) ? "[OK] Found relation: song\n" : "[!!] Missing relation: song\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("song->Find('recordid=1' ... ADODB_JOIN_AR) [Join Method]\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $song = new Song(); + $songs = $song->Find('recordid=1', false, false, array('loading' => ADODB_WORK_AR)); + ar_echo((ar_assert(found($songs, "'name' => 'No Hiding Place'"))) ? "[OK] Found song\n" : "[!!] Find failed\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("artist->Find('artistuniqueid=1' ... ADODB_LAZY_AR) [Lazy Method]\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $artist = new Artist(); + $artists = $artist->Find('artistuniqueid=1', false, false, array('loading' => ADODB_LAZY_AR)); + ar_echo((ar_assert(found($artists, "'name' => 'Elvis Costello'"))) ? "[OK] Found Elvis Costello\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(notfound($artists, "'name' => 'No Hiding Place'"))) ? "[OK] No relation yet\n" : "[!!] Found relation when I shouldn't\n"); + foreach($artists as $anartist) + { + foreach($anartist->songs as $asong) + { + if($asong->name); + } + } + ar_echo((ar_assert(found($artists, "'name' => 'No Hiding Place'"))) ? "[OK] Found relation: song\n" : "[!!] Missing relation: song\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("song->Find('recordid=1' ... ADODB_LAZY_AR) [Lazy Method]\n"); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); + $song = new Song(); + $songs = $song->Find('recordid=1', false, false, array('loading' => ADODB_LAZY_AR)); + ar_echo((ar_assert(found($songs, "'name' => 'No Hiding Place'"))) ? "[OK] Found song\n" : "[!!] Find failed\n"); + ar_echo((ar_assert(notfound($songs, "'name' => 'Elvis Costello'"))) ? "[OK] No relation yet\n" : "[!!] Found relation when I shouldn't\n"); + foreach($songs as $asong) + { + if($asong->artist); + } + ar_echo((ar_assert(found($songs, "'name' => 'Elvis Costello'"))) ? "[OK] Found relation: artist\n" : "[!!] Missing relation: artist\n"); + + ar_echo("\n\n-------------------------------------------------------------------------------------------------------------------\n"); + ar_echo("Test suite complete. " . (($err_count > 0) ? "$err_count errors found.\n" : "Success.\n")); + ar_echo("-------------------------------------------------------------------------------------------------------------------\n"); diff --git a/app/vendor/adodb/adodb-php/tests/test-datadict.php b/app/vendor/adodb/adodb-php/tests/test-datadict.php new file mode 100644 index 000000000..9c7422aa3 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test-datadict.php @@ -0,0 +1,251 @@ +$dbType

"; + $db = NewADOConnection($dbType); + $dict = NewDataDictionary($db); + + if (!$dict) continue; + $dict->debug = 1; + + $opts = array('REPLACE','mysql' => 'ENGINE=INNODB', 'oci8' => 'TABLESPACE USERS'); + +/* $flds = array( + array('id', 'I', + 'AUTO','KEY'), + + array('name' => 'firstname', 'type' => 'varchar','size' => 30, + 'DEFAULT'=>'Joan'), + + array('lastname','varchar',28, + 'DEFAULT'=>'Chen','key'), + + array('averylonglongfieldname','X',1024, + 'NOTNULL','default' => 'test'), + + array('price','N','7.2', + 'NOTNULL','default' => '0.00'), + + array('MYDATE', 'D', + 'DEFDATE'), + array('TS','T', + 'DEFTIMESTAMP') + );*/ + + $flds = " +ID I AUTO KEY, +FIRSTNAME VARCHAR(30) DEFAULT 'Joan' INDEX idx_name, +LASTNAME VARCHAR(28) DEFAULT 'Chen' key INDEX idx_name INDEX idx_lastname, +averylonglongfieldname X(1024) DEFAULT 'test', +price N(7.2) DEFAULT '0.00', +MYDATE D DEFDATE INDEX idx_date, +BIGFELLOW X NOTNULL, +TS_SECS T DEFTIMESTAMP, +TS_SUBSEC TS DEFTIMESTAMP +"; + + + $sqla = $dict->CreateDatabase('KUTU',array('postgres'=>"LOCATION='/u01/postdata'")); + $dict->SetSchema('KUTU'); + + $sqli = ($dict->CreateTableSQL('testtable',$flds, $opts)); + $sqla = array_merge($sqla,$sqli); + + $sqli = $dict->CreateIndexSQL('idx','testtable','price,firstname,lastname',array('BITMAP','FULLTEXT','CLUSTERED','HASH')); + $sqla = array_merge($sqla,$sqli); + $sqli = $dict->CreateIndexSQL('idx2','testtable','price,lastname');//,array('BITMAP','FULLTEXT','CLUSTERED')); + $sqla = array_merge($sqla,$sqli); + + $addflds = array(array('height', 'F'),array('weight','F')); + $sqli = $dict->AddColumnSQL('testtable',$addflds); + $sqla = array_merge($sqla,$sqli); + $addflds = array(array('height', 'F','NOTNULL'),array('weight','F','NOTNULL')); + $sqli = $dict->AlterColumnSQL('testtable',$addflds); + $sqla = array_merge($sqla,$sqli); + + + printsqla($dbType,$sqla); + + if (file_exists('d:\inetpub\wwwroot\php\phplens\adodb\adodb.inc.php')) + if ($dbType == 'mysqlt') { + $db->Connect('localhost', "root", "", "test"); + $dict->SetSchema(''); + $sqla2 = $dict->ChangeTableSQL('adoxyz',$flds); + if ($sqla2) printsqla($dbType,$sqla2); + } + if ($dbType == 'postgres') { + if (@$db->Connect('localhost', "tester", "test", "test")); + $dict->SetSchema(''); + $sqla2 = $dict->ChangeTableSQL('adoxyz',$flds); + if ($sqla2) printsqla($dbType,$sqla2); + } + + if ($dbType == 'odbc_mssql') { + $dsn = $dsn = "PROVIDER=MSDASQL;Driver={SQL Server};Server=localhost;Database=northwind;"; + if (@$db->Connect($dsn, "sa", "natsoft", "test")); + $dict->SetSchema(''); + $sqla2 = $dict->ChangeTableSQL('adoxyz',$flds); + if ($sqla2) printsqla($dbType,$sqla2); + } + + + + adodb_pr($dict->databaseType); + printsqla($dbType, $dict->DropColumnSQL('table',array('my col','`col2_with_Quotes`','A_col3','col3(10)'))); + printsqla($dbType, $dict->ChangeTableSQL('adoxyz','LASTNAME varchar(32)')); + +} + +function printsqla($dbType,$sqla) +{ + print "

";
+	//print_r($dict->MetaTables());
+	foreach($sqla as $s) {
+		$s = htmlspecialchars($s);
+		print "$s;\n";
+		if ($dbType == 'oci8') print "/\n";
+	}
+	print "

"; +} + +/*** + +Generated SQL: + +mysql + +CREATE DATABASE KUTU; +DROP TABLE KUTU.testtable; +CREATE TABLE KUTU.testtable ( +id INTEGER NOT NULL AUTO_INCREMENT, +firstname VARCHAR(30) DEFAULT 'Joan', +lastname VARCHAR(28) NOT NULL DEFAULT 'Chen', +averylonglongfieldname LONGTEXT NOT NULL, +price NUMERIC(7,2) NOT NULL DEFAULT 0.00, +MYDATE DATE DEFAULT CURDATE(), + PRIMARY KEY (id, lastname) +)TYPE=ISAM; +CREATE FULLTEXT INDEX idx ON KUTU.testtable (firstname,lastname); +CREATE INDEX idx2 ON KUTU.testtable (price,lastname); +ALTER TABLE KUTU.testtable ADD height DOUBLE; +ALTER TABLE KUTU.testtable ADD weight DOUBLE; +ALTER TABLE KUTU.testtable MODIFY COLUMN height DOUBLE NOT NULL; +ALTER TABLE KUTU.testtable MODIFY COLUMN weight DOUBLE NOT NULL; + + +-------------------------------------------------------------------------------- + +oci8 + +CREATE USER KUTU IDENTIFIED BY tiger; +/ +GRANT CREATE SESSION, CREATE TABLE,UNLIMITED TABLESPACE,CREATE SEQUENCE TO KUTU; +/ +DROP TABLE KUTU.testtable CASCADE CONSTRAINTS; +/ +CREATE TABLE KUTU.testtable ( +id NUMBER(16) NOT NULL, +firstname VARCHAR(30) DEFAULT 'Joan', +lastname VARCHAR(28) DEFAULT 'Chen' NOT NULL, +averylonglongfieldname CLOB NOT NULL, +price NUMBER(7,2) DEFAULT 0.00 NOT NULL, +MYDATE DATE DEFAULT TRUNC(SYSDATE), + PRIMARY KEY (id, lastname) +)TABLESPACE USERS; +/ +DROP SEQUENCE KUTU.SEQ_testtable; +/ +CREATE SEQUENCE KUTU.SEQ_testtable; +/ +CREATE OR REPLACE TRIGGER KUTU.TRIG_SEQ_testtable BEFORE insert ON KUTU.testtable + FOR EACH ROW + BEGIN + select KUTU.SEQ_testtable.nextval into :new.id from dual; + END; +/ +CREATE BITMAP INDEX idx ON KUTU.testtable (firstname,lastname); +/ +CREATE INDEX idx2 ON KUTU.testtable (price,lastname); +/ +ALTER TABLE testtable ADD ( + height NUMBER, + weight NUMBER); +/ +ALTER TABLE testtable MODIFY( + height NUMBER NOT NULL, + weight NUMBER NOT NULL); +/ + + +-------------------------------------------------------------------------------- + +postgres +AlterColumnSQL not supported for PostgreSQL + + +CREATE DATABASE KUTU LOCATION='/u01/postdata'; +DROP TABLE KUTU.testtable; +CREATE TABLE KUTU.testtable ( +id SERIAL, +firstname VARCHAR(30) DEFAULT 'Joan', +lastname VARCHAR(28) DEFAULT 'Chen' NOT NULL, +averylonglongfieldname TEXT NOT NULL, +price NUMERIC(7,2) DEFAULT 0.00 NOT NULL, +MYDATE DATE DEFAULT CURRENT_DATE, + PRIMARY KEY (id, lastname) +); +CREATE INDEX idx ON KUTU.testtable USING HASH (firstname,lastname); +CREATE INDEX idx2 ON KUTU.testtable (price,lastname); +ALTER TABLE KUTU.testtable ADD height FLOAT8; +ALTER TABLE KUTU.testtable ADD weight FLOAT8; + + +-------------------------------------------------------------------------------- + +odbc_mssql + +CREATE DATABASE KUTU; +DROP TABLE KUTU.testtable; +CREATE TABLE KUTU.testtable ( +id INT IDENTITY(1,1) NOT NULL, +firstname VARCHAR(30) DEFAULT 'Joan', +lastname VARCHAR(28) DEFAULT 'Chen' NOT NULL, +averylonglongfieldname TEXT NOT NULL, +price NUMERIC(7,2) DEFAULT 0.00 NOT NULL, +MYDATE DATETIME DEFAULT GetDate(), + PRIMARY KEY (id, lastname) +); +CREATE CLUSTERED INDEX idx ON KUTU.testtable (firstname,lastname); +CREATE INDEX idx2 ON KUTU.testtable (price,lastname); +ALTER TABLE KUTU.testtable ADD + height REAL, + weight REAL; +ALTER TABLE KUTU.testtable ALTER COLUMN height REAL NOT NULL; +ALTER TABLE KUTU.testtable ALTER COLUMN weight REAL NOT NULL; + + +-------------------------------------------------------------------------------- +*/ + + +echo "

Test XML Schema

"; +$ff = file('xmlschema.xml'); +echo "
";
+foreach($ff as $xml) echo htmlspecialchars($xml);
+echo "
"; +include_once('test-xmlschema.php'); diff --git a/app/vendor/adodb/adodb-php/tests/test-perf.php b/app/vendor/adodb/adodb-php/tests/test-perf.php new file mode 100644 index 000000000..62465bef6 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test-perf.php @@ -0,0 +1,48 @@ + $v) { + if (strncmp($k,'test',4) == 0) $_SESSION['_db'] = $k; + } +} + +if (isset($_SESSION['_db'])) { + $_db = $_SESSION['_db']; + $_GET[$_db] = 1; + $$_db = 1; +} + +echo "

Performance Monitoring

"; +include_once('testdatabases.inc.php'); + + +function testdb($db) +{ + if (!$db) return; + echo "";print_r($db->ServerInfo()); echo " user=".$db->user.""; + + $perf = NewPerfMonitor($db); + + # unit tests + if (0) { + //$DB->debug=1; + echo "Data Cache Size=".$perf->DBParameter('data cache size').'

'; + echo $perf->HealthCheck(); + echo($perf->SuspiciousSQL()); + echo($perf->ExpensiveSQL()); + echo($perf->InvalidSQL()); + echo $perf->Tables(); + + echo "

";
+		echo $perf->HealthCheckCLI();
+		$perf->Poll(3);
+		die();
+	}
+
+	if ($perf) $perf->UI(3);
+}
diff --git a/app/vendor/adodb/adodb-php/tests/test-pgblob.php b/app/vendor/adodb/adodb-php/tests/test-pgblob.php
new file mode 100644
index 000000000..3add99e64
--- /dev/null
+++ b/app/vendor/adodb/adodb-php/tests/test-pgblob.php
@@ -0,0 +1,86 @@
+Param(false);
+		$x = (rand() % 10) + 1;
+		$db->debug= ($i==1);
+		$id = $db->GetOne($sql,
+			array('Z%','Z%',$x));
+		if($id != $offset+$x) {
+			print "

Error at $x"; + break; + } + } +} + +include_once('../adodb.inc.php'); +$db = NewADOConnection('postgres7'); +$db->PConnect('localhost','tester','test','test') || die("failed connection"); + +$enc = "GIF89a%01%00%01%00%80%FF%00%C0%C0%C0%00%00%00%21%F9%04%01%00%00%00%00%2C%00%00%00%00%01%00%01%00%00%01%012%00%3Bt_clear.gif%0D"; +$val = rawurldecode($enc); + +$MAX = 1000; + +adodb_pr($db->ServerInfo()); + +echo "

Testing PREPARE/EXECUTE PLAN

"; + + +$db->_bindInputArray = true; // requires postgresql 7.3+ and ability to modify database +$t = getmicrotime(); +doloop(); +echo '

',$MAX,' times, with plan=',getmicrotime() - $t,'

'; + + +$db->_bindInputArray = false; +$t = getmicrotime(); +doloop(); +echo '

',$MAX,' times, no plan=',getmicrotime() - $t,'

'; + + + +echo "

Testing UPDATEBLOB

"; +$db->debug=1; + +### TEST BEGINS + +$db->Execute("insert into photos (id,name) values(9999,'dot.gif')"); +$db->UpdateBlob('photos','photo',$val,'id=9999'); +$v = $db->GetOne('select photo from photos where id=9999'); + + +### CLEANUP + +$db->Execute("delete from photos where id=9999"); + +### VALIDATION + +if ($v !== $val) echo "*** ERROR: Inserted value does not match downloaded val"; +else echo "*** OK: Passed"; + +echo "
";
+echo "INSERTED: ", $enc;
+echo "
"; +echo"RETURNED: ", rawurlencode($v); +echo "

"; +echo "INSERTED: ", $val; +echo "


"; +echo "RETURNED: ", $v; diff --git a/app/vendor/adodb/adodb-php/tests/test-php5.php b/app/vendor/adodb/adodb-php/tests/test-php5.php new file mode 100644 index 000000000..a8f1ffe5c --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test-php5.php @@ -0,0 +1,116 @@ +PHP ".PHP_VERSION."\n"; +try { + +$dbt = 'oci8po'; + +try { +switch($dbt) { +case 'oci8po': + $db = NewADOConnection("oci8po"); + + $db->Connect('localhost','scott','natsoft','sherkhan'); + break; +default: +case 'mysql': + $db = NewADOConnection("mysql"); + $db->Connect('localhost','root','','northwind'); + break; + +case 'mysqli': + $db = NewADOConnection("mysqli://root:@localhost/northwind"); + //$db->Connect('localhost','root','','test'); + break; +} +} catch (exception $e){ + echo "Connect Failed"; + adodb_pr($e); + die(); +} + +$db->debug=1; + +$cnt = $db->GetOne("select count(*) from adoxyz where ?Prepare("select * from adoxyz where ?ErrorMsg(),"\n"; +$rs = $db->Execute($stmt,array(10,20)); + +echo "
Foreach Iterator Test (rand=".rand().")
"; +$i = 0; +foreach($rs as $v) { + $i += 1; + echo "rec $i: "; $s1 = adodb_pr($v,true); $s2 = adodb_pr($rs->fields,true); + if ($s1 != $s2 && !empty($v)) {adodb_pr($s1); adodb_pr($s2);} + else echo "passed
"; + flush(); +} + +$rs = new ADORecordSet_empty(); +foreach($rs as $v) { + echo "

empty ";var_dump($v); +} + + +if ($i != $cnt) die("actual cnt is $i, cnt should be $cnt\n"); +else echo "Count $i is correct
"; + +$rs = $db->Execute("select bad from badder"); + +} catch (exception $e) { + adodb_pr($e); + echo "

adodb_backtrace:

\n"; + $e = adodb_backtrace($e->gettrace()); +} + +$rs = $db->Execute("select distinct id, firstname,lastname from adoxyz order by id"); +echo "Result=\n",$rs,"

"; + +echo "

Active Record

"; + + include_once("../adodb-active-record.inc.php"); + ADOdb_Active_Record::SetDatabaseAdapter($db); + +try { + class City extends ADOdb_Active_Record{}; + $a = new City(); + +} catch(exception $e){ + echo $e->getMessage(); +} + +try { + + $a = new City(); + + echo "

Successfully created City()
"; + #var_dump($a->GetPrimaryKeys()); + $a->city = 'Kuala Lumpur'; + $a->Save(); + $a->Update(); + #$a->SetPrimaryKeys(array('city')); + $a->country = "M'sia"; + $a->save(); + $a->Delete(); +} catch(exception $e){ + echo $e->getMessage(); +} + +//include_once("test-active-record.php"); diff --git a/app/vendor/adodb/adodb-php/tests/test-xmlschema.php b/app/vendor/adodb/adodb-php/tests/test-xmlschema.php new file mode 100644 index 000000000..c56cfec8e --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test-xmlschema.php @@ -0,0 +1,53 @@ +Connect( 'localhost', 'root', '', 'test' ) || die('fail connect1'); + +// To create a schema object and build the query array. +$schema = new adoSchema( $db ); + +// To upgrade an existing schema object, use the following +// To upgrade an existing database to the provided schema, +// uncomment the following line: +#$schema->upgradeSchema(); + +print "SQL to build xmlschema.xml:\n

";
+// Build the SQL array
+$sql = $schema->ParseSchema( "xmlschema.xml" );
+
+var_dump( $sql );
+print "
\n"; + +// Execute the SQL on the database +//$result = $schema->ExecuteSchema( $sql ); + +// Finally, clean up after the XML parser +// (PHP won't do this for you!) +//$schema->Destroy(); + + + +print "SQL to build xmlschema-mssql.xml:\n
";
+
+$db2 = ADONewConnection('mssql');
+$db2->Connect('','adodb','natsoft','northwind') || die("Fail 2");
+
+$db2->Execute("drop table simple_table");
+
+$schema = new adoSchema( $db2 );
+$sql = $schema->ParseSchema( "xmlschema-mssql.xml" );
+
+print_r( $sql );
+print "
\n"; + +$db2->debug=1; + +foreach ($sql as $s) +$db2->Execute($s); diff --git a/app/vendor/adodb/adodb-php/tests/test.php b/app/vendor/adodb/adodb-php/tests/test.php new file mode 100644 index 000000000..50e74ad4c --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test.php @@ -0,0 +1,1781 @@ +$msg

"; + flush(); +} + +function CheckWS($conn) +{ +global $ADODB_EXTENSION; + + include_once('../session/adodb-session.php'); + if (defined('CHECKWSFAIL')){ echo " TESTING $conn ";flush();} + $saved = $ADODB_EXTENSION; + $db = ADONewConnection($conn); + $ADODB_EXTENSION = $saved; + if (headers_sent()) { + print "

White space detected in adodb-$conn.inc.php or include file...

"; + //die(); + } +} + +function do_strtolower(&$arr) +{ + foreach($arr as $k => $v) { + if (is_object($v)) $arr[$k] = adodb_pr($v,true); + else $arr[$k] = strtolower($v); + } +} + + +function CountExecs($db, $sql, $inputarray) +{ +global $EXECS; $EXECS++; +} + +function CountCachedExecs($db, $secs2cache, $sql, $inputarray) +{ +global $CACHED; $CACHED++; +} + +// the table creation code is specific to the database, so we allow the user +// to define their own table creation stuff + +function testdb(&$db,$createtab="create table ADOXYZ (id int, firstname char(24), lastname char(24), created date)") +{ +GLOBAL $ADODB_vers,$ADODB_CACHE_DIR,$ADODB_FETCH_MODE,$ADODB_COUNTRECS; + + //adodb_pr($db); + +?>
+

+
 
+

+Execute('select lastname,firstname,lastname,id from ADOXYZ'); + $arr = $rs->GetAssoc(); + echo "
";print_r($arr);
+	die();*/
+
+	if (!$db) die("testdb: database not inited");
+	GLOBAL $EXECS, $CACHED;
+
+	$EXECS = 0;
+	$CACHED = 0;
+	//$db->Execute("drop table adodb_logsql");
+	if ((rand()%3) == 0) @$db->Execute("delete from adodb_logsql");
+	$db->debug=1;
+
+	$db->fnExecute = 'CountExecs';
+	$db->fnCacheExecute = 'CountCachedExecs';
+
+	if (empty($_GET['nolog'])) {
+		echo "

SQL Logging enabled

"; + $db->LogSQL();/* + $sql = +"SELECT t1.sid, t1.sid, t1.title, t1.hometext, t1.notes, t1.aid, t1.informant, +t2.url, t2.email, t1.catid, t3.title, t1.topic, t4.topicname, t4.topicimage, +t4.topictext, t1.score, t1.ratings, t1.counter, t1.comments, t1.acomm +FROM `nuke_stories` `t1`, `nuke_authors` `t2`, `nuke_stories_cat` `t3`, `nuke_topics` `t4` + WHERE ((t2.aid=t1.aid) AND (t3.catid=t1.catid) AND (t4.topicid=t1.topic) + AND ((t1.alanguage='german') OR (t1.alanguage='')) AND (t1.ihome='0')) + ORDER BY t1.time DESC"; + $db->SelectLimit($sql); + echo $db->ErrorMsg();*/ + } + $ADODB_CACHE_DIR = dirname(TempNam('/tmp','testadodb')); + $db->debug = false; + //print $db->UnixTimeStamp('2003-7-22 23:00:00'); + + $phpv = phpversion(); + if (defined('ADODB_EXTENSION')) $ext = '   Extension '.ADODB_EXTENSION.' installed'; + else $ext = ''; + print "

ADODB Version: $ADODB_vers"; + print "

Host: $db->host"; + print "
Database: $db->database"; + print "
PHP: $phpv $ext

"; + + flush(); + + print "Current timezone: " . date_default_timezone_get() . "

"; + + $arr = $db->ServerInfo(); + print_r($arr); + echo E_ALL,' ',E_STRICT, "
"; + $e = error_reporting(E_ALL | E_STRICT); + echo error_reporting(),'

'; + flush(); + #$db->debug=1; + $tt = $db->Time(); + if ($tt == 0) echo '
$db->Time failed'; + else echo "
db->Time: ".date('d-m-Y H:i:s',$tt); + echo '
'; + + echo "Date=",$db->UserDate('2002-04-07'),'
'; + print "date1 (1969-02-20) = ".$db->DBDate('1969-2-20'); + print "
date1 (1999-02-20) = ".$db->DBDate('1999-2-20'); + print "
date1.1 1999 injection attack= ".$db->DBDate("'1999', ' injection attack '"); + print "
date2 (1970-1-2) = ".$db->DBDate(24*3600)."

"; + print "ts1 (1999-02-20 13:40:50) = ".$db->DBTimeStamp('1999-2-20 1:40:50 pm'); + print "
ts1.1 (1999-02-20 13:40:00) = ".$db->DBTimeStamp('1999-2-20 13:40'); + print "
ts2 (1999-02-20) = ".$db->DBTimeStamp('1999-2-20'); + print "
ts2 (1999-02-20) = ".$db->DBTimeStamp("'1999-2-20', 'injection attack'"); + print "
ts3 (1970-1-2 +/- timezone) = ".$db->DBTimeStamp(24*3600); + print "
Fractional TS (1999-2-20 13:40:50.91): ".$db->DBTimeStamp($db->UnixTimeStamp('1999-2-20 13:40:50.91+1')); + $dd = $db->UnixDate('1999-02-20'); + print "
unixdate 1999-02-20 = ".date('Y-m-d',$dd)."

"; + print "
ts4 =".($db->UnixTimeStamp("19700101000101")+8*3600); + print "
ts5 =".$db->DBTimeStamp($db->UnixTimeStamp("20040110092123")); + print "
ts6 =".$db->UserTimeStamp("20040110092123"); + print "
ts7 =".$db->DBTimeStamp("20040110092123"); + flush(); + // mssql too slow in failing bad connection + if (false && $db->databaseType != 'mssql') { + print "

Testing bad connection. Ignore following error msgs:
"; + $db2 = ADONewConnection(); + $rez = $db2->Connect("bad connection"); + $err = $db2->ErrorMsg(); + print "Error='$err'

"; + if ($rez) print "Cannot check if connection failed. The Connect() function returned true.

"; + } + #error_reporting($e); + flush(); + + //$ADODB_COUNTRECS=false; + $rs=$db->Execute('select * from ADOXYZ order by id'); + if($rs === false) $create = true; + else $rs->Close(); + + //if ($db->databaseType !='vfp') $db->Execute("drop table ADOXYZ"); + + if ($create) { + if (false && $db->databaseType == 'ibase') { + print "Please create the following table for testing:

$createtab

"; + return; + } else { + $db->debug = 99; + # $e = error_reporting(E_ALL-E_WARNING); + $db->Execute($createtab); + # error_reporting($e); + } + } + #error_reporting(E_ALL); + echo "

Testing Metatypes

"; + $t = $db->MetaType('varchar'); + if ($t != 'C') Err("Bad Metatype for varchar"); + + $rs = $db->Execute("delete from ADOXYZ"); // some ODBC drivers will fail the drop so we delete + if ($rs) { + if(! $rs->EOF) print "Error: RecordSet returned by Execute('delete...') should show EOF

"; + $rs->Close(); + } else print "err=".$db->ErrorMsg(); + + print "

Test select on empty table, FetchField when EOF, and GetInsertSQL

"; + $rs = $db->Execute("select id,firstname from ADOXYZ where id=9999"); + if ($rs && !$rs->EOF) print "Error: RecordSet returned by Execute(select...') on empty table should show EOF

"; + if ($rs->EOF && (($ox = $rs->FetchField(0)) && !empty($ox->name))) { + $record['id'] = 99; + $record['firstname'] = 'John'; + $sql = $db->GetInsertSQL($rs, $record); + if (strtoupper($sql) != strtoupper("INSERT INTO ADOXYZ ( id, firstname ) VALUES ( 99, 'John' )")) Err("GetInsertSQL does not work on empty table: $sql"); + } else { + Err("FetchField does not work on empty recordset, meaning GetInsertSQL will fail..."); + } + if ($rs) $rs->Close(); + flush(); + //$db->debug=true; + print "

Testing Commit: "; + $time = $db->DBDate(time()); + if (!$db->BeginTrans()) { + print 'Transactions not supported

'; + if ($db->hasTransactions) Err("hasTransactions should be false"); + } else { /* COMMIT */ + if (!$db->hasTransactions) Err("hasTransactions should be true"); + if ($db->transCnt != 1) Err("Invalid transCnt = $db->transCnt (should be 1)"); + $rs = $db->Execute("insert into ADOXYZ (id,firstname,lastname,created) values (99,'Should Not','Exist (Commit)',$time)"); + if ($rs && $db->CommitTrans()) { + $rs->Close(); + $rs = $db->Execute("select * from ADOXYZ where id=99"); + if ($rs === false || $rs->EOF) { + print 'Data not saved

'; + $rs = $db->Execute("select * from ADOXYZ where id=99"); + print_r($rs); + die(); + } else print 'OK

'; + if ($rs) $rs->Close(); + } else { + if (!$rs) { + print "Insert failed

"; + $db->RollbackTrans(); + } else print "Commit failed

"; + } + if ($db->transCnt != 0) Err("Invalid transCnt = $db->transCnt (should be 0)"); + + /* ROLLBACK */ + if (!$db->BeginTrans()) print "

Error in BeginTrans()

"; + print "

Testing Rollback: "; + $db->Execute("insert into ADOXYZ (id,firstname,lastname,created) values (100,'Should Not','Exist (Rollback)',$time)"); + if ($db->RollbackTrans()) { + $rs = $db->Execute("select * from ADOXYZ where id=100"); + if ($rs && !$rs->EOF) print 'Fail: Data should rollback

'; + else print 'OK

'; + if ($rs) $rs->Close(); + } else + print "Commit failed

"; + + $rs = $db->Execute('delete from ADOXYZ where id>50'); + if ($rs) $rs->Close(); + + if ($db->transCnt != 0) Err("Invalid transCnt = $db->transCnt (should be 0)"); + } + + if (1) { + print "

Testing MetaDatabases()

"; + print_r( $db->MetaDatabases()); + + print "

Testing MetaTables() and MetaColumns()

"; + $a = $db->MetaTables(); + if ($a===false) print "MetaTables not supported

"; + else { + print "Array of tables and views: "; + foreach($a as $v) print " ($v) "; + print '

'; + } + + $a = $db->MetaTables('VIEW'); + if ($a===false) print "MetaTables not supported (views)

"; + else { + print "Array of views: "; + foreach($a as $v) print " ($v) "; + print '

'; + } + + $a = $db->MetaTables(false,false,'aDo%'); + if ($a===false) print "MetaTables not supported (mask)

"; + else { + print "Array of ado%: "; + foreach($a as $v) print " ($v) "; + print '

'; + } + + $a = $db->MetaTables('TABLE'); + if ($a===false) print "MetaTables not supported

"; + else { + print "Array of tables: "; + foreach($a as $v) print " ($v) "; + print '

'; + } + + $db->debug=0; + $rez = $db->MetaColumns("NOSUCHTABLEHERE"); + if ($rez !== false) { + Err("MetaColumns error handling failed"); + var_dump($rez); + } + $db->debug=1; + $a = $db->MetaColumns('ADOXYZ'); + if ($a===false) print "MetaColumns not supported

"; + else { + print "

Columns of ADOXYZ:
"; + foreach($a as $v) {print_r($v); echo "
";} + echo "
"; + } + + print "

Testing MetaIndexes

"; + + $a = $db->MetaIndexes(('ADOXYZ'),true); + if ($a===false) print "MetaIndexes not supported

"; + else { + print "

Indexes of ADOXYZ:
"; + adodb_pr($a); + echo "
"; + } + print "

Testing MetaPrimaryKeys

"; + $a = $db->MetaPrimaryKeys('ADOXYZ'); + var_dump($a); + } + $rs = $db->Execute('delete from ADOXYZ'); + if ($rs) $rs->Close(); + + $db->debug = false; + + + switch ($db->databaseType) { + case 'vfp': + + if (0) { + // memo test + $rs = $db->Execute("select data from memo"); + rs2html($rs); + } + break; + + case 'postgres7': + case 'postgres64': + case 'postgres': + case 'ibase': + print "

Encode=".$db->BlobEncode("abc\0d\"' +ef")."

";//' + + print "

Testing Foreign Keys

"; + $arr = $db->MetaForeignKeys('ADOXYZ',false,true); + print_r($arr); + if (!$arr) Err("No MetaForeignKeys"); + break; + + case 'odbc_mssql': + case 'mssqlpo': + print "

Testing Foreign Keys

"; + $arr = $db->MetaForeignKeys('Orders',false,true); + print_r($arr); + if (!$arr) Err("Bad MetaForeignKeys"); + if ($db->databaseType == 'odbc_mssql') break; + + case 'mssql': + + +/* +ASSUME Northwind available... + +CREATE PROCEDURE SalesByCategory + @CategoryName nvarchar(15), @OrdYear nvarchar(4) = '1998' +AS +IF @OrdYear != '1996' AND @OrdYear != '1997' AND @OrdYear != '1998' +BEGIN + SELECT @OrdYear = '1998' +END + +SELECT ProductName, + TotalPurchase=ROUND(SUM(CONVERT(decimal(14,2), OD.Quantity * (1-OD.Discount) * OD.UnitPrice)), 0) +FROM [Order Details] OD, Orders O, Products P, Categories C +WHERE OD.OrderID = O.OrderID + AND OD.ProductID = P.ProductID + AND P.CategoryID = C.CategoryID + AND C.CategoryName = @CategoryName + AND SUBSTRING(CONVERT(nvarchar(22), O.OrderDate, 111), 1, 4) = @OrdYear +GROUP BY ProductName +ORDER BY ProductName +GO + + +CREATE PROCEDURE ADODBTestSP +@a nvarchar(25) +AS +SELECT GETDATE() AS T, @a AS A +GO +*/ + print "

Testing Stored Procedures for mssql

"; + $saved = $db->debug; + $db->debug=true; + $assoc = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $cmd = $db->PrepareSP('ADODBTestSP'); + $ss = "You should see me in the output."; + $db->InParameter($cmd,$ss,'a'); + $rs = $db->Execute($cmd); + #var_dump($rs->fields); + echo $rs->fields['T']." --- ".$rs->fields['A']."---
"; + + $cat = 'Dairy Products'; + $yr = '1998'; + + $stmt = $db->PrepareSP('SalesByCategory'); + $db->InParameter($stmt,$cat,'CategoryName'); + $db->InParameter($stmt,$yr,'OrdYear'); + $rs = $db->Execute($stmt); + rs2html($rs); + + $cat = 'Grains/Cereals'; + $yr = 1998; + + $stmt = $db->PrepareSP('SalesByCategory'); + $db->InParameter($stmt,$cat,'CategoryName'); + $db->InParameter($stmt,$yr,'OrdYear'); + $rs = $db->Execute($stmt); + rs2html($rs); + + $ADODB_FETCH_MODE = $assoc; + + /* + Test out params - works in PHP 4.2.3 and 4.3.3 and 4.3.8 but not 4.3.0: + + CREATE PROCEDURE at_date_interval + @days INTEGER, + @start VARCHAR(20) OUT, + @end VARCHAR(20) OUT + AS + BEGIN + set @start = CONVERT(VARCHAR(20), getdate(), 101) + set @end =CONVERT(VARCHAR(20), dateadd(day, @days, getdate()), 101 ) + END + GO + */ + $db->debug=1; + $stmt = $db->PrepareSP('at_date_interval'); + $days = 10; + $begin_date = ''; + $end_date = ''; + $db->InParameter($stmt,$days,'days', 4, SQLINT4); + $db->OutParameter($stmt,$begin_date,'start', 20, SQLVARCHAR ); + $db->OutParameter($stmt,$end_date,'end', 20, SQLVARCHAR ); + $db->Execute($stmt); + if (empty($begin_date) or empty($end_date) or $begin_date == $end_date) { + Err("MSSQL SP Test for OUT Failed"); + print "begin=$begin_date end=$end_date

"; + } else print "(Today +10days) = (begin=$begin_date end=$end_date)

"; + + $db->debug = $saved; + break; + case 'oci8': + case 'oci8po': + + if (0) { + $t = getmicrotime(); + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $arr = $db->GetArray('select * from abalone_tree'); + $arr = $db->GetArray('select * from abalone_tree'); + $arr = $db->GetArray('select * from abalone_tree'); + echo "

t = ",getmicrotime() - $t,"

"; + die(); + } + + # cleanup + $db->Execute("delete from photos where id=99 or id=1"); + $db->Execute("insert into photos (id) values(1)"); + $db->Execute("update photos set photo=null,descclob=null where id=1"); + + $saved = $db->debug; + $db->debug=true; + + + + /* + CREATE TABLE PHOTOS + ( + ID NUMBER(16) primary key, + PHOTO BLOB, + DESCRIPTION VARCHAR2(4000 BYTE), + DESCCLOB CLOB + ); + + INSERT INTO PHOTOS (ID) VALUES(1); + */ + $s = ''; + for ($i = 0; $i <= 500; $i++) { + $s .= '1234567890'; + } + + $sql = "INSERT INTO photos ( ID, photo) ". + "VALUES ( :id, empty_blob() )". + " RETURNING photo INTO :xx"; + + + $blob_data = $s; + $id = 99; + + $stmt = $db->PrepareSP($sql); + $db->InParameter($stmt, $id, 'id'); + $blob = $db->InParameter($stmt, $s, 'xx',-1, OCI_B_BLOB); + $db->StartTrans(); + $result = $db->Execute($stmt); + $db->CompleteTrans(); + + $s2= $db->GetOne("select photo from photos where id=99"); + echo "
---$s2"; + if ($s !== $s2) Err("insert blob does not match"); + + print "

Testing Blob: size=".strlen($s)."

"; + $ok = $db->Updateblob('photos','photo',$s,'id=1'); + if (!$ok) Err("Blob failed 1"); + else { + $s2= $db->GetOne("select photo from photos where id=1"); + if ($s !== $s2) Err("updateblob does not match"); + } + + print "

Testing Clob: size=".strlen($s)."

"; + $ok = $db->UpdateClob('photos','descclob',$s,'id=1'); + if (!$ok) Err("Clob failed 1"); + else { + $s2= $db->GetOne("select descclob from photos where id=1"); + if ($s !== $s2) Err("updateclob does not match"); + } + + + $s = ''; + $s2 = ''; + print "

Testing Foreign Keys

"; + $arr = $db->MetaForeignKeys('emp','scott'); + print_r($arr); + if (!$arr) Err("Bad MetaForeignKeys"); +/* +-- TEST PACKAGE +-- "Set scan off" turns off substitution variables. +Set scan off; + +CREATE OR REPLACE PACKAGE Adodb AS +TYPE TabType IS REF CURSOR RETURN TAB%ROWTYPE; +PROCEDURE open_tab (tabcursor IN OUT TabType,tablenames IN VARCHAR); +PROCEDURE open_tab2 (tabcursor IN OUT TabType,tablenames IN OUT VARCHAR) ; +PROCEDURE data_out(input IN VARCHAR, output OUT VARCHAR); +PROCEDURE data_in(input IN VARCHAR); +PROCEDURE myproc (p1 IN NUMBER, p2 OUT NUMBER); +END Adodb; +/ + + +CREATE OR REPLACE PACKAGE BODY Adodb AS +PROCEDURE open_tab (tabcursor IN OUT TabType,tablenames IN VARCHAR) IS + BEGIN + OPEN tabcursor FOR SELECT * FROM TAB WHERE tname LIKE tablenames; + END open_tab; + + PROCEDURE open_tab2 (tabcursor IN OUT TabType,tablenames IN OUT VARCHAR) IS + BEGIN + OPEN tabcursor FOR SELECT * FROM TAB WHERE tname LIKE tablenames; + tablenames := 'TEST'; + END open_tab2; + +PROCEDURE data_out(input IN VARCHAR, output OUT VARCHAR) IS + BEGIN + output := 'Cinta Hati '||input; + END; + +PROCEDURE data_in(input IN VARCHAR) IS + ignore varchar(1000); + BEGIN + ignore := input; + END; + +PROCEDURE myproc (p1 IN NUMBER, p2 OUT NUMBER) AS +BEGIN +p2 := p1; +END; +END Adodb; +/ + +*/ + + print "

Testing Cursor Variables

"; + $rs = $db->ExecuteCursor("BEGIN adodb.open_tab(:zz,'A%'); END;",'zz'); + + if ($rs && !$rs->EOF) { + $v = $db->GetOne("SELECT count(*) FROM tab where tname like 'A%'"); + if ($v == $rs->RecordCount()) print "Test 1 RowCount: OK

"; + else Err("Test 1 RowCount ".$rs->RecordCount().", actual = $v"); + } else { + print "Error in using Cursor Variables 1

"; + } + if ($rs) $rs->Close(); + + print "

Testing Stored Procedures for oci8

"; + + $stmt = $db->PrepareSP("BEGIN adodb.data_out(:a1, :a2); END;"); + $a1 = 'Malaysia'; + //$a2 = ''; # a2 doesn't even need to be defined! + $db->InParameter($stmt,$a1,'a1'); + $db->OutParameter($stmt,$a2,'a2'); + $rs = $db->Execute($stmt); + if ($rs) { + if ($a2 !== 'Cinta Hati Malaysia') print "Stored Procedure Error: a2 = $a2

"; + else echo "OK: a2=$a2

"; + } else { + print "Error in using Stored Procedure IN/Out Variables

"; + } + + $tname = 'A%'; + + $stmt = $db->PrepareSP('select * from tab where tname like :tablename'); + $db->Parameter($stmt,$tname,'tablename'); + $rs = $db->Execute($stmt); + rs2html($rs); + + $stmt = $db->PrepareSP("begin adodb.data_in(:a1); end;"); + $db->InParameter($stmt,$a1,'a1'); + $db->Execute($stmt); + + $db->debug = $saved; + break; + + default: + break; + } + $arr = array( + array(1,'Caroline','Miranda'), + array(2,'John','Lim'), + array(3,'Wai Hun','See') + ); + //$db->debug=1; + print "

Testing Bulk Insert of 3 rows

"; + +// $db->debug=1; +// $db->Execute('select * from table where val=? AND value=?', array('val'=>'http ://www.whatever.com/test?=21', 'value'=>'blabl')); + + + $sql = "insert into ADOXYZ (id,firstname,lastname) values (".$db->Param('0').",".$db->Param('1').",".$db->Param('2').")"; + $db->bulkBind = true; + $db->StartTrans(); + $db->debug=99; + $db->Execute($sql,$arr); + $db->CompleteTrans(); + $db->bulkBind = false; + $rs = $db->Execute('select * from ADOXYZ order by id'); + if (!$rs || $rs->RecordCount() != 3) Err("Bad bulk insert"); + + rs2html($rs); + + $db->Execute('delete from ADOXYZ'); + + print "

Inserting 50 rows

"; + + for ($i = 0; $i < 5; $i++) { + + $time = $db->DBDate(time()); + if (empty($_GET['hide'])) $db->debug = true; + switch($db->databaseType){ + case 'mssqlpo': + case 'mssql': + $sqlt = "CREATE TABLE mytable ( + row1 INT IDENTITY(1,1) NOT NULL, + row2 varchar(16), + PRIMARY KEY (row1))"; + //$db->debug=1; + if (!$db->Execute("delete from mytable")) + $db->Execute($sqlt); + + $ok = $db->Execute("insert into mytable (row2) values ('test')"); + $ins_id=$db->Insert_ID(); + echo "Insert ID=";var_dump($ins_id); + if ($ins_id == 0) Err("Bad Insert_ID()"); + $ins_id2 = $db->GetOne("select row1 from mytable"); + if ($ins_id != $ins_id2) Err("Bad Insert_ID() 2"); + + $arr = array(0=>'Caroline',1=>'Miranda'); + $sql = "insert into ADOXYZ (id,firstname,lastname,created) values ($i*10+0,?,?,$time)"; + break; + case 'mysqli': + case 'mysqlt': + case 'mysql': + $sqlt = "CREATE TABLE `mytable` ( + `row1` int(11) NOT NULL auto_increment, + `row2` varchar(16) NOT NULL default '', + PRIMARY KEY (`row1`), + KEY `myindex` (`row1`,`row2`) +) "; + if (!$db->Execute("delete from mytable")) + $db->Execute($sqlt); + + $ok = $db->Execute("insert into mytable (row2) values ('test')"); + $ins_id=$db->Insert_ID(); + echo "Insert ID=";var_dump($ins_id); + if ($ins_id == 0) Err("Bad Insert_ID()"); + $ins_id2 = $db->GetOne("select row1 from mytable"); + if ($ins_id != $ins_id2) Err("Bad Insert_ID() 2"); + + default: + $arr = array(0=>'Caroline',1=>'Miranda'); + $sql = "insert into ADOXYZ (id,firstname,lastname,created) values ($i*10+0,?,?,$time)"; + break; + + case 'oci8': + case 'oci805': + $arr = array('first'=>'Caroline','last'=>'Miranda'); + $amt = rand() % 100; + $sql = "insert into ADOXYZ (id,firstname,lastname,created) values ($i*10+0,:first,:last,$time)"; + break; + } + if ($i & 1) { + $sql = $db->Prepare($sql); + } + $rs = $db->Execute($sql,$arr); + + if ($rs === false) Err( 'Error inserting with parameters'); + else $rs->Close(); + $db->debug = false; + $db->Execute("insert into ADOXYZ (id,firstname,lastname,created) values ($i*10+1,'John','Lim',$time)"); + /*$ins_id=$db->Insert_ID(); + echo "Insert ID=";var_dump($ins_id);*/ + if ($db->databaseType == 'mysql') if ($ins_id == 0) Err('Bad Insert_ID'); + $db->Execute("insert into ADOXYZ (id,firstname,lastname,created) values ($i*10+2,'Mary','Lamb',$time )"); + $db->Execute("insert into ADOXYZ (id,firstname,lastname,created) values ($i*10+3,'George','Washington',$time )"); + $db->Execute("insert into ADOXYZ (id,firstname,lastname,created) values ($i*10+4,'Mr. Alan','Tam',$time )"); + $db->Execute("insert into ADOXYZ (id,firstname,lastname,created) values ($i*10+5,'Alan',".$db->quote("Turing'ton").",$time )"); + $db->Execute("insert into ADOXYZ (id,firstname,lastname,created)values ($i*10+6,'Serena','Williams',$time )"); + $db->Execute("insert into ADOXYZ (id,firstname,lastname,created) values ($i*10+7,'Yat Sun','Sun',$time )"); + $db->Execute("insert into ADOXYZ (id,firstname,lastname,created) values ($i*10+8,'Wai Hun','See',$time )"); + $db->Execute("insert into ADOXYZ (id,firstname,lastname,created) values ($i*10+9,'Steven','Oey',$time )"); + } // for + if (1) { + $db->debug=1; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $cnt = $db->GetOne("select count(*) from ADOXYZ"); + $rs = $db->Execute('update ADOXYZ set id=id+1'); + if (!is_object($rs)) { + print_r($rs); + err("Update should return object"); + } + if (!$rs) err("Update generated error"); + + $nrows = $db->Affected_Rows(); + if ($nrows === false) print "

Affected_Rows() not supported

"; + else if ($nrows != $cnt) print "

Affected_Rows() Error: $nrows returned (should be 50)

"; + else print "

Affected_Rows() passed

"; + } + + if ($db->dataProvider == 'oci8') $array = array('zid'=>1,'zdate'=>date('Y-m-d',time())); + else $array=array(1,date('Y-m-d',time())); + + + #$array = array(1,date('Y-m-d',time())); + $id = $db->GetOne("select id from ADOXYZ + where id=".$db->Param('zid')." and created>=".$db->Param('ZDATE')."", + $array); + if ($id != 1) Err("Bad bind; id=$id"); + else echo "
Bind date/integer 1 passed"; + + $array =array(1,$db->BindDate(time())); + $id = $db->GetOne("select id from ADOXYZ + where id=".$db->Param('0')." and created>=".$db->Param('1')."", + $array); + if ($id != 1) Err("Bad bind; id=$id"); + else echo "
Bind date/integer 2 passed"; + + $db->debug = false; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + ////////////////////////////////////////////////////////////////////////////////////////// + + $rs = $db->Execute("select * from ADOXYZ where firstname = 'not known'"); + if (!$rs || !$rs->EOF) print "

Error on empty recordset

"; + else if ($rs->RecordCount() != 0) { + print "

Error on RecordCount. Should be 0. Was ".$rs->RecordCount()."

"; + print_r($rs->fields); + } + if ($db->databaseType !== 'odbc') { + $rs = $db->Execute("select id,firstname,lastname,created,".$db->random." from ADOXYZ order by id"); + if ($rs) { + if ($rs->RecordCount() != 50) { + print "

RecordCount returns ".$rs->RecordCount().", should be 50

"; + adodb_pr($rs->GetArray()); + $poc = $rs->PO_RecordCount('ADOXYZ'); + if ($poc == 50) print "

    PO_RecordCount passed

"; + else print "

PO_RecordCount returns wrong value: $poc

"; + } else print "

RecordCount() passed

"; + if (isset($rs->fields['firstname'])) print '

The fields columns can be indexed by column name.

'; + else { + Err( '

The fields columns cannot be indexed by column name.

'); + print_r($rs->fields); + } + if (empty($_GET['hide'])) rs2html($rs); + } + else print "

Error in Execute of SELECT with random

"; + } + $val = $db->GetOne("select count(*) from ADOXYZ"); + if ($val == 50) print "

GetOne returns ok

"; + else print "

Fail: GetOne returns $val

"; + + echo "GetRow Test"; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $val1 = $db->GetRow("select count(*) from ADOXYZ"); + $val2 = $db->GetRow("select count(*) from ADOXYZ"); + if ($val1[0] == 50 and sizeof($val1) == 1 and $val2[0] == 50 and sizeof($val2) == 1) print "

GetRow returns ok

"; + else { + print_r($val); + print "

Fail: GetRow returns {$val2[0]}

"; + } + + print "

FetchObject/FetchNextObject Test

"; + $rs = $db->Execute('select * from ADOXYZ'); + if ($rs) { + if (empty($rs->connection)) print "Connection object missing from recordset
"; + + while ($o = $rs->FetchNextObject()) { // calls FetchObject internally + if (!is_string($o->FIRSTNAME) || !is_string($o->LASTNAME)) { + print_r($o); + print "

Firstname is not string

"; + break; + } + } + } else { + print "

Failed rs

"; + die("

ADOXYZ table cannot be read - die()"); + } + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + print "

FetchObject/FetchNextObject Test 2

"; + #$db->debug=99; + $rs = $db->Execute('select * from ADOXYZ'); + if (empty($rs->connection)) print "Connection object missing from recordset
"; + print_r($rs->fields); + while ($o = $rs->FetchNextObject()) { // calls FetchObject internally + if (!is_string($o->FIRSTNAME) || !is_string($o->LASTNAME)) { + print_r($o); + print "

Firstname is not string

"; + break; + } + } + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + $savefetch = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + + print "

CacheSelectLimit Test...

"; + $rs = $db->CacheSelectLimit('select id, firstname from ADOXYZ order by id',2); + + if (ADODB_ASSOC_CASE == 2 || $db->dataProvider == 'oci8') { + $id = 'ID'; + $fname = 'FIRSTNAME'; + }else { + $id = 'id'; + $fname = 'firstname'; + } + + + if ($rs && !$rs->EOF) { + if (isset($rs->fields[0])) { + Err("ASSOC has numeric fields"); + print_r($rs->fields); + } + if ($rs->fields[$id] != 1) {Err("Error"); print_r($rs->fields);}; + if (trim($rs->fields[$fname]) != 'Caroline') {print Err("Error 2"); print_r($rs->fields);}; + + $rs->MoveNext(); + if ($rs->fields[$id] != 2) {Err("Error 3"); print_r($rs->fields);}; + $rs->MoveNext(); + if (!$rs->EOF) { + Err("Error EOF"); + print_r($rs); + } + } + + print "

FETCH_MODE = ASSOC: Should get 1, Caroline ASSOC_CASE=".ADODB_ASSOC_CASE."

"; + $rs = $db->SelectLimit('select id,firstname from ADOXYZ order by id',2); + if ($rs && !$rs->EOF) { + + if ($rs->fields[$id] != 1) {Err("Error 1"); print_r($rs->fields);}; + if (trim($rs->fields[$fname]) != 'Caroline') {Err("Error 2"); print_r($rs->fields);}; + $rs->MoveNext(); + if ($rs->fields[$id] != 2) {Err("Error 3"); print_r($rs->fields);}; + $rs->MoveNext(); + if (!$rs->EOF) Err("Error EOF"); + else if (is_array($rs->fields) || $rs->fields) { + Err("Error: ## fields should be set to false on EOF"); + print_r($rs->fields); + } + } + + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + print "

FETCH_MODE = NUM: Should get 1, Caroline

"; + $rs = $db->SelectLimit('select id,firstname from ADOXYZ order by id',1); + if ($rs && !$rs->EOF) { + if (isset($rs->fields[$id])) Err("FETCH_NUM has ASSOC fields"); + if ($rs->fields[0] != 1) {Err("Error 1"); print_r($rs->fields);}; + if (trim($rs->fields[1]) != 'Caroline') {Err("Error 2");print_r($rs->fields);}; + $rs->MoveNext(); + if (!$rs->EOF) Err("Error EOF"); + + } + $ADODB_FETCH_MODE = $savefetch; + + $db->debug = false; + print "

GetRowAssoc Upper: Should get 1, Caroline

"; + $rs = $db->SelectLimit('select id,firstname from ADOXYZ order by id',1); + if ($rs && !$rs->EOF) { + $arr = $rs->GetRowAssoc(ADODB_ASSOC_CASE_UPPER); + + if ($arr[strtoupper($id)] != 1) {Err("Error 1");print_r($arr);}; + if (trim($arr[strtoupper($fname)]) != 'Caroline') {Err("Error 2"); print_r($arr);}; + $rs->MoveNext(); + if (!$rs->EOF) Err("Error EOF"); + + } + print "

GetRowAssoc Lower: Should get 1, Caroline

"; + $rs = $db->SelectLimit('select id,firstname from ADOXYZ order by id',1); + if ($rs && !$rs->EOF) { + $arr = $rs->GetRowAssoc(ADODB_ASSOC_CASE_LOWER); + if ($arr['id'] != 1) {Err("Error 1"); print_r($arr);}; + if (trim($arr['firstname']) != 'Caroline') {Err("Error 2"); print_r($arr);}; + + } + + print "

GetCol Test

"; + $col = $db->GetCol('select distinct firstname from ADOXYZ order by 1'); + if (!is_array($col)) Err("Col size is wrong"); + if (trim($col[0]) != 'Alan' or trim($col[9]) != 'Yat Sun') Err("Col elements wrong"); + + + $col = $db->CacheGetCol('select distinct firstname from ADOXYZ order by 1'); + if (!is_array($col)) Err("Col size is wrong"); + if (trim($col[0]) != 'Alan' or trim($col[9]) != 'Yat Sun') Err("Col elements wrong"); + + $db->debug = true; + + + echo "

Date Update Test

"; + $zdate = date('Y-m-d',time()+3600*24); + $zdate = $db->DBDate($zdate); + $db->Execute("update ADOXYZ set created=$zdate where id=1"); + $row = $db->GetRow("select created,firstname from ADOXYZ where id=1"); + print_r($row); echo "
"; + + + + print "

SelectLimit Distinct Test 1: Should see Caroline, John and Mary

"; + $rs = $db->SelectLimit('select distinct * from ADOXYZ order by id',3); + + + if ($rs && !$rs->EOF) { + if (trim($rs->fields[1]) != 'Caroline') Err("Error 1 (exp Caroline), ".$rs->fields[1]); + $rs->MoveNext(); + + if (trim($rs->fields[1]) != 'John') Err("Error 2 (exp John), ".$rs->fields[1]); + $rs->MoveNext(); + if (trim($rs->fields[1]) != 'Mary') Err("Error 3 (exp Mary),".$rs->fields[1]); + $rs->MoveNext(); + if (! $rs->EOF) Err("Error EOF"); + //rs2html($rs); + } else Err("Failed SelectLimit Test 1"); + + print "

SelectLimit Test 2: Should see Mary, George and Mr. Alan

"; + $rs = $db->SelectLimit('select * from ADOXYZ order by id',3,2); + if ($rs && !$rs->EOF) { + if (trim($rs->fields[1]) != 'Mary') Err("Error 1 - No Mary, instead: ".$rs->fields[1]); + $rs->MoveNext(); + if (trim($rs->fields[1]) != 'George')Err("Error 2 - No George, instead: ".$rs->fields[1]); + $rs->MoveNext(); + if (trim($rs->fields[1]) != 'Mr. Alan') Err("Error 3 - No Mr. Alan, instead: ".$rs->fields[1]); + $rs->MoveNext(); + if (! $rs->EOF) Err("Error EOF"); + // rs2html($rs); + } + else Err("Failed SelectLimit Test 2 ". ($rs ? 'EOF':'no RS')); + + print "

SelectLimit Test 3: Should see Wai Hun and Steven

"; + $db->debug=1; + global $A; $A=1; + $rs = $db->SelectLimit('select * from ADOXYZ order by id',-1,48); + $A=0; + if ($rs && !$rs->EOF) { + if (empty($rs->connection)) print "Connection object missing from recordset
"; + if (trim($rs->fields[1]) != 'Wai Hun') Err("Error 1 ".$rs->fields[1]); + $rs->MoveNext(); + if (trim($rs->fields[1]) != 'Steven') Err("Error 2 ".$rs->fields[1]); + $rs->MoveNext(); + if (! $rs->EOF) { + Err("Error EOF"); + } + //rs2html($rs); + } + else Err("Failed SelectLimit Test 3"); + $db->debug = false; + + + $rs = $db->Execute("select * from ADOXYZ order by id"); + print "

Testing Move()

"; + if (!$rs)Err( "Failed Move SELECT"); + else { + if (!$rs->Move(2)) { + if (!$rs->canSeek) print "

$db->databaseType: Move(), MoveFirst() nor MoveLast() not supported.

"; + else print '

RecordSet->canSeek property should be set to false

'; + } else { + $rs->MoveFirst(); + if (trim($rs->Fields("firstname")) != 'Caroline') { + print "

$db->databaseType: MoveFirst failed -- probably cannot scroll backwards

"; + } + else print "MoveFirst() OK
"; + + // Move(3) tests error handling -- MoveFirst should not move cursor + $rs->Move(3); + if (trim($rs->Fields("firstname")) != 'George') { + print '

'.$rs->Fields("id")."$db->databaseType: Move(3) failed

"; + } else print "Move(3) OK
"; + + $rs->Move(7); + if (trim($rs->Fields("firstname")) != 'Yat Sun') { + print '

'.$rs->Fields("id")."$db->databaseType: Move(7) failed

"; + print_r($rs); + } else print "Move(7) OK
"; + if ($rs->EOF) Err("Move(7) is EOF already"); + $rs->MoveLast(); + if (trim($rs->Fields("firstname")) != 'Steven'){ + print '

'.$rs->Fields("id")."$db->databaseType: MoveLast() failed

"; + print_r($rs); + }else print "MoveLast() OK
"; + $rs->MoveNext(); + if (!$rs->EOF) err("Bad MoveNext"); + if ($rs->canSeek) { + $rs->Move(3); + if (trim($rs->Fields("firstname")) != 'George') { + print '

'.$rs->Fields("id")."$db->databaseType: Move(3) after MoveLast failed

"; + + } else print "Move(3) after MoveLast() OK
"; + } + + print "

Empty Move Test"; + $rs = $db->Execute("select * from ADOXYZ where id > 0 and id < 0"); + $rs->MoveFirst(); + if (!$rs->EOF || $rs->fields) Err("Error in empty move first"); + } + } + + $rs = $db->Execute('select * from ADOXYZ where id = 2'); + if ($rs->EOF || !is_array($rs->fields)) Err("Error in select"); + $rs->MoveNext(); + if (!$rs->EOF) Err("Error in EOF (xx) "); + // $db->debug=true; + print "

Testing ADODB_FETCH_ASSOC and concat: concat firstname and lastname

"; + + $save = $ADODB_FETCH_MODE; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + if ($db->dataProvider == 'postgres') { + $sql = "select ".$db->Concat('cast(firstname as varchar)',$db->qstr(' '),'lastname')." as fullname,id,".$db->sysTimeStamp." as d from ADOXYZ"; + $rs = $db->Execute($sql); + } else { + $sql = "select distinct ".$db->Concat('firstname',$db->qstr(' '),'lastname')." as fullname,id,".$db->sysTimeStamp." as d from ADOXYZ"; + $rs = $db->Execute($sql); + } + if ($rs) { + if (empty($_GET['hide'])) rs2html($rs); + } else { + Err( "Failed Concat:".$sql); + } + $ADODB_FETCH_MODE = $save; + print "
Testing GetArray() "; + //$ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + + $rs = $db->Execute("select * from ADOXYZ order by id"); + if ($rs) { + $arr = $rs->GetArray(10); + if (sizeof($arr) != 10 || trim($arr[1][1]) != 'John' || trim($arr[1][2]) != 'Lim') print $arr[1][1].' '.$arr[1][2]."   ERROR
"; + else print " OK
"; + } + + $arr = $db->GetArray("select x from ADOXYZ"); + $e = $db->ErrorMsg(); $e2 = $db->ErrorNo(); + echo "Testing error handling, should see illegal column 'x' error=$e ($e2)
"; + if (!$e || !$e2) Err("Error handling did not work"); + print "Testing FetchNextObject for 1 object "; + $rs = $db->Execute("select distinct lastname,firstname from ADOXYZ where firstname='Caroline'"); + $fcnt = 0; + if ($rs) + while ($o = $rs->FetchNextObject()) { + $fcnt += 1; + } + if ($fcnt == 1) print " OK
"; + else print "FAILED
"; + + $stmt = $db->Prepare("select * from ADOXYZ where id < 3"); + $rs = $db->Execute($stmt); + if (!$rs) Err("Prepare failed"); + else { + $arr = $rs->GetArray(); + if (!$arr) Err("Prepare failed 2"); + if (sizeof($arr) != 2) Err("Prepare failed 3"); + } + print "Testing GetAssoc() "; + + + if ($db->dataProvider == 'mysql') { + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $arr = $db->GetAssoc("SELECT 'adodb', '0'"); + var_dump($arr); + die(); + } + + $savecrecs = $ADODB_COUNTRECS; + $ADODB_COUNTRECS = false; + //$arr = $db->GetArray("select lastname,firstname from ADOXYZ"); + //print_r($arr); + print "
"; + $rs = $db->Execute("select distinct lastname,firstname,created from ADOXYZ"); + + if ($rs) { + $arr = $rs->GetAssoc(); + //print_r($arr); + if (empty($arr['See']) || trim(reset($arr['See'])) != 'Wai Hun') print $arr['See']."   ERROR
"; + else print " OK 1"; + } + + $arr = $db->GetAssoc("select distinct lastname,firstname from ADOXYZ"); + if ($arr) { + //print_r($arr); + if (empty($arr['See']) || trim($arr['See']) != 'Wai Hun') print $arr['See']."   ERROR
"; + else print " OK 2
"; + } + // Comment this out to test countrecs = false + $ADODB_COUNTRECS = $savecrecs; + $db->debug=1; + $query = $db->Prepare("select count(*) from ADOXYZ"); + $rs = $db->CacheExecute(10,$query); + if (reset($rs->fields) != 50) echo Err("$cnt wrong for Prepare/CacheGetOne"); + + for ($loop=0; $loop < 1; $loop++) { + print "Testing GetMenu() and CacheExecute
"; + $db->debug = true; + $rs = $db->CacheExecute(4,"select distinct firstname,lastname from ADOXYZ"); + + + + + if ($rs) print 'With blanks, Steven selected:'. $rs->GetMenu('menu','Steven').'
'; + else print " Fail
"; + $rs = $db->CacheExecute(4,"select distinct firstname,lastname from ADOXYZ"); + + if ($rs) print ' No blanks, Steven selected: '. $rs->GetMenu('menu','Steven',false).'
'; + else print " Fail
"; + + $rs = $db->CacheExecute(4,"select distinct firstname,lastname from ADOXYZ"); + + if ($rs) print ' 1st line set to **** , Steven selected: '. $rs->GetMenu('menu','Steven','1st:****').'
'; + else print " Fail
"; + + + + $rs = $db->CacheExecute(4,"select distinct firstname,lastname from ADOXYZ"); + if ($rs) print ' Multiple, Alan selected: '. $rs->GetMenu('menu','Alan',false,true).'
'; + else print " Fail
"; + print '


'; + + $rs = $db->CacheExecute(4,"select distinct firstname,lastname from ADOXYZ"); + if ($rs) { + print ' Multiple, Alan and George selected: '. $rs->GetMenu('menu',array('Alan','George'),false,true); + if (empty($rs->connection)) print "Connection object missing from recordset
"; + } else print " Fail
"; + print '


'; + + print "Testing GetMenu3()
"; + $rs = $db->Execute("select ".$db->Concat('firstname',"'-'",'id').",id, lastname from ADOXYZ order by lastname,id"); + if ($rs) print "Grouped Menu: ".$rs->GetMenu3('name'); + else Err('Grouped Menu GetMenu3()'); + print "
"; + + print "Testing GetMenu2()
"; + $rs = $db->CacheExecute(4,"select distinct firstname,lastname from ADOXYZ"); + if ($rs) print 'With blanks, Steven selected:'. $rs->GetMenu2('menu',('Oey')).'
'; + else print " Fail
"; + $rs = $db->CacheExecute(6,"select distinct firstname,lastname from ADOXYZ"); + if ($rs) print ' No blanks, Steven selected: '. $rs->GetMenu2('menu',('Oey'),false).'
'; + else print " Fail
"; + } + echo "

CacheExecute

"; + + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $rs = $db->CacheExecute(6,"select distinct firstname,lastname from ADOXYZ"); + print_r($rs->fields); echo $rs->fetchMode;echo "
"; + echo $rs->Fields($fname); + + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $rs = $db->CacheExecute(6,"select distinct firstname,lastname from ADOXYZ"); + print_r($rs->fields);echo "
"; + echo $rs->Fields($fname); + $db->debug = false; + + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + // phplens + + $sql = 'select * from ADOXYZ where 0=1'; + echo "

**Testing '$sql' (phplens compat 1)

"; + $rs = $db->Execute($sql); + if (!$rs) err( "No recordset returned for '$sql'"); + if (!$rs->FieldCount()) err( "No fields returned for $sql"); + if (!$rs->FetchField(1)) err( "FetchField failed for $sql"); + + $sql = 'select * from ADOXYZ order by 1'; + echo "

**Testing '$sql' (phplens compat 2)

"; + $rs = $db->Execute($sql); + if (!$rs) err( "No recordset returned for '$sql'
".$db->ErrorMsg()."
"); + + + $sql = 'select * from ADOXYZ order by 1,1'; + echo "

**Testing '$sql' (phplens compat 3)

"; + $rs = $db->Execute($sql); + if (!$rs) err( "No recordset returned for '$sql'
".$db->ErrorMsg()."
"); + + + // Move + $rs1 = $db->Execute("select id from ADOXYZ where id <= 2 order by 1"); + $rs2 = $db->Execute("select id from ADOXYZ where id = 3 or id = 4 order by 1"); + + if ($rs1) $rs1->MoveLast(); + if ($rs2) $rs2->MoveLast(); + + if (empty($rs1) || empty($rs2) || $rs1->fields[0] != 2 || $rs2->fields[0] != 4) { + $a = $rs1->fields[0]; + $b = $rs2->fields[0]; + print "

Error in multiple recordset test rs1=$a rs2=$b (should be rs1=2 rs2=4)

"; + } else + print "

Testing multiple recordsets OK

"; + + + echo "

GenID test: "; + for ($i=1; $i <= 10; $i++) + echo "($i: ",$val = $db->GenID($db->databaseType.'abcseq7' ,5), ") "; + if ($val == 0) Err("GenID not supported"); + + if ($val) { + $db->DropSequence('abc_seq2'); + $db->CreateSequence('abc_seq2'); + $val = $db->GenID('abc_seq2'); + $db->DropSequence('abc_seq2'); + $db->CreateSequence('abc_seq2'); + $val = $db->GenID('abc_seq2'); + if ($val != 1) Err("Drop and Create Sequence not supported ($val)"); + } + echo "

"; + + if (substr($db->dataProvider,0,3) != 'notused') { // used to crash ado + $sql = "select firstnames from ADOXYZ"; + print "

Testing execution of illegal statement: $sql

"; + if ($db->Execute($sql) === false) { + print "

This returns the following ErrorMsg(): ".$db->ErrorMsg()." and ErrorNo(): ".$db->ErrorNo().'

'; + } else + print "

Error in error handling -- Execute() should return false

"; + } else + print "

ADO skipped error handling of bad select statement

"; + + print "

ASSOC TEST 2
"; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $rs = $db->query('select * from ADOXYZ order by id'); + if ($ee = $db->ErrorMsg()) { + Err("Error message=$ee"); + } + if ($ee = $db->ErrorNo()) { + Err("Error No = $ee"); + } + print_r($rs->fields); + for($i=0;$i<$rs->FieldCount();$i++) + { + $fld=$rs->FetchField($i); + print "
Field name is ".$fld->name; + print " ".$rs->Fields($fld->name); + } + + + print "

BOTH TEST 2
"; + if ($db->dataProvider == 'ado') { + print "ADODB_FETCH_BOTH not supported for dataProvider=".$db->dataProvider."
"; + } else { + $ADODB_FETCH_MODE = ADODB_FETCH_BOTH; + $rs = $db->query('select * from ADOXYZ order by id'); + for($i=0;$i<$rs->FieldCount();$i++) + { + $fld=$rs->FetchField($i); + print "
Field name is ".$fld->name; + print " ".$rs->Fields($fld->name); + } + } + + print "

NUM TEST 2
"; + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $rs = $db->query('select * from ADOXYZ order by id'); + for($i=0;$i<$rs->FieldCount();$i++) + { + $fld=$rs->FetchField($i); + print "
Field name is ".$fld->name; + print " ".$rs->Fields($fld->name); + } + + print "

ASSOC Test of SelectLimit
"; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $rs = $db->selectlimit('select * from ADOXYZ order by id',3,4); + $cnt = 0; + while ($rs && !$rs->EOF) { + $cnt += 1; + if (!isset($rs->fields['firstname'])) { + print "
ASSOC returned numeric field

"; + break; + } + $rs->MoveNext(); + } + if ($cnt != 3) print "
Count should be 3, instead it was $cnt

"; + + + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + if ($db->sysDate) { + $saved = $db->debug; + $db->debug = 1; + $rs = $db->Execute("select {$db->sysDate} from ADOXYZ where id=1"); + if (ADORecordSet::UnixDate(date('Y-m-d')) != $rs->UnixDate($rs->fields[0])) { + print "

Invalid date {$rs->fields[0]}

"; + } else + print "

Passed \$sysDate test ({$rs->fields[0]})

"; + + print_r($rs->FetchField(0)); + print time(); + $db->debug=$saved; + } else { + print "

\$db->sysDate not defined

"; + } + + print "

Test CSV

"; + include_once('../toexport.inc.php'); + //$ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $rs = $db->SelectLimit('select id,firstname,lastname,created,\'He, he\' he,\'"\' q from ADOXYZ',10); + + print "
";
+	print rs2csv($rs);
+	print "
"; + + $rs = $db->SelectLimit('select id,firstname,lastname,created,\'The "young man", he said\' from ADOXYZ',10); + + if (PHP_VERSION < 5) { + print "
";
+		rs2tabout($rs);
+		print "
"; + } + #print " CacheFlush "; + #$db->CacheFlush(); + + $date = $db->SQLDate('d-m-M-Y-\QQ h:i:s A'); + $sql = "SELECT $date from ADOXYZ"; + print "

Test SQLDate: ".htmlspecialchars($sql)."

"; + $rs = $db->SelectLimit($sql,1); + $d = date('d-m-M-Y-').'Q'.(ceil(date('m')/3.0)).date(' h:i:s A'); + if (!$rs) Err("SQLDate query returned no recordset"); + else if ($d != $rs->fields[0]) Err("SQLDate 1 failed expected:
act:$d
sql:".$rs->fields[0]); + + $dbdate = $db->DBDate("1974-02-25"); + if (substr($db->dataProvider, 0, 8) == 'postgres') { + $dbdate .= "::TIMESTAMP"; + } + + $date = $db->SQLDate('d-m-M-Y-\QQ h:i:s A', $dbdate); + $sql = "SELECT $date from ADOXYZ"; + print "

Test SQLDate: ".htmlspecialchars($sql)."

"; + $db->debug=1; + $rs = $db->SelectLimit($sql,1); + $ts = ADOConnection::UnixDate('1974-02-25'); + $d = date('d-m-M-Y-',$ts).'Q'.(ceil(date('m',$ts)/3.0)).date(' h:i:s A',$ts); + if (!$rs) { + Err("SQLDate query returned no recordset"); + echo $db->ErrorMsg(),'
'; + } else if ($d != reset($rs->fields)) { + Err("SQLDate 2 failed expected:
act:$d
sql:".$rs->fields[0].'
'.$db->ErrorMsg()); + } + + + print "

Test Filter

"; + $db->debug = 1; + + $rs = $db->SelectLimit('select * from ADOXYZ where id < 3 order by id'); + + $rs = RSFilter($rs,'do_strtolower'); + if (trim($rs->fields[1]) != 'caroline' && trim($rs->fields[2]) != 'miranda') { + err('**** RSFilter failed'); + print_r($rs->fields); + } + + rs2html($rs); + + $db->debug=1; + + + print "

Test Replace

"; + + $ret = $db->Replace('ADOXYZ', + array('id'=>1,'firstname'=>'Caroline','lastname'=>'Miranda'), + array('id'), + $autoq = true); + if (!$ret) echo "

Error in replacing existing record

"; + else { + $saved = $db->debug; + $db->debug = 0; + $savec = $ADODB_COUNTRECS; + $ADODB_COUNTRECS = true; + $rs = $db->Execute('select * FROM ADOXYZ where id=1'); + $db->debug = $saved; + if ($rs->RecordCount() != 1) { + $cnt = $rs->RecordCount(); + rs2html($rs); + print "Error - Replace failed, count=$cnt

"; + } + $ADODB_COUNTRECS = $savec; + } + $ret = $db->Replace('ADOXYZ', + array('id'=>1000,'firstname'=>'Harun','lastname'=>'Al-Rashid'), + array('id','firstname'), + $autoq = true); + if ($ret != 2) print "Replace failed: "; + print "test A return value=$ret (2 expected)

"; + + $ret = $db->Replace('ADOXYZ', + array('id'=>1000,'firstname'=>'Sherazade','lastname'=>'Al-Rashid'), + 'id', + $autoq = true); + if ($ret != 1) + if ($db->dataProvider == 'ibase' && $ret == 2); + else print "Replace failed: "; + print "test B return value=$ret (1 or if ibase then 2 expected)

"; + + print "

rs2rs Test

"; + + $rs = $db->Execute('select * from ADOXYZ where id>= 1 order by id'); + $rs = $db->_rs2rs($rs); + $rs->valueX = 'X'; + $rs->MoveNext(); + $rs = $db->_rs2rs($rs); + if (!isset($rs->valueX)) err("rs2rs does not preserve array recordsets"); + if (reset($rs->fields) != 1) err("rs2rs does not move to first row: id=".reset($rs->fields)); + + ///////////////////////////////////////////////////////////// + include_once('../pivottable.inc.php'); + print "

Pivot Test

"; + $db->debug=true; + $sql = PivotTableSQL( + $db, # adodb connection + 'ADOXYZ', # tables + 'firstname', # row fields + 'lastname', # column fields + false, # join + 'ID', # sum + 'Sum ', # label for sum + 'sum', # aggregate function + true + ); + $rs = $db->Execute($sql); + if ($rs) rs2html($rs); + else Err("Pivot sql error"); + + $pear = false; //true; + $db->debug=false; + + if ($pear) { + // PEAR TESTS BELOW + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + + include_once "PEAR.php"; + $rs = $db->query('select * from ADOXYZ where id>0 and id<10 order by id'); + + $i = 0; + if ($rs && !$rs->EOF) { + while ($arr = $rs->fetchRow()) { + $i++; + //print "$i "; + if ($arr[0] != $i) { + print_r($arr); + print "

PEAR DB emulation error 1.

"; + $pear = false; + break; + } + } + $rs->Close(); + } + + + if ($i != $db->GetOne('select count(*) from ADOXYZ where id>0 and id<10')) { + print "

PEAR DB emulation error 1.1 EOF ($i)

"; + $pear = false; + } + + $rs = $db->limitQuery('select * from ADOXYZ where id>0 order by id',$i=3,$top=3); + $i2 = $i; + if ($rs && !$rs->EOF) { + + while (!is_object($rs->fetchInto($arr))) { + $i2++; + + // print_r($arr); + // print "$i ";print_r($arr); + if ($arr[0] != $i2) { + print "

PEAR DB emulation error 2.

"; + $pear = false; + break; + } + } + $rs->Close(); + } + if ($i2 != $i+$top) { + print "

PEAR DB emulation error 2.1 EOF (correct=$i+$top, actual=$i2)

"; + $pear = false; + } + } + if ($pear) print "

PEAR DB emulation passed.

"; + flush(); + + + $rs = $db->SelectLimit("select ".$db->sysDate." from ADOXYZ",1); + $date = $rs->fields[0]; + if (!$date) Err("Bad sysDate"); + else { + $ds = $db->UserDate($date,"d m Y"); + if ($ds != date("d m Y")) Err("Bad UserDate: ".$ds.' expected='.date("d m Y")); + else echo "Passed UserDate: $ds

"; + } + $db->debug=1; + if ($db->dataProvider == 'oci8') + $rs = $db->SelectLimit("select to_char(".$db->sysTimeStamp.",'YYYY-MM-DD HH24:MI:SS') from ADOXYZ",1); + else + $rs = $db->SelectLimit("select ".$db->sysTimeStamp." from ADOXYZ",1); + $date = $rs->fields[0]; + if (!$date) Err("Bad sysTimeStamp"); + else { + $ds = $db->UserTimeStamp($date,"H \\h\\r\\s-d m Y"); + if ($ds != date("H \\h\\r\\s-d m Y")) Err("Bad UserTimeStamp: ".$ds.", correct is ".date("H \\h\\r\\s-d m Y")); + else echo "Passed UserTimeStamp: $ds

"; + + $date = 100; + $ds = $db->UserTimeStamp($date,"H \\h\\r\\s-d m Y"); + $ds2 = date("H \\h\\r\\s-d m Y",$date); + if ($ds != $ds2) Err("Bad UserTimeStamp 2: $ds: $ds2"); + else echo "Passed UserTimeStamp 2: $ds

"; + } + flush(); + + if ($db->hasTransactions) { + $db->debug=1; + echo "

Testing StartTrans CompleteTrans

"; + $db->raiseErrorFn = false; + + $db->SetTransactionMode('SERIALIZABLE'); + $db->StartTrans(); + $rs = $db->Execute('select * from notable'); + $db->StartTrans(); + $db->BeginTrans(); + $db->Execute("update ADOXYZ set firstname='Carolx' where id=1"); + $db->CommitTrans(); + $db->CompleteTrans(); + $rez = $db->CompleteTrans(); + $db->SetTransactionMode(''); + $db->debug=0; + if ($rez !== false) { + if (is_null($rez)) Err("Error: _transOK not modified"); + else Err("Error: CompleteTrans (1) should have failed"); + } else { + $name = $db->GetOne("Select firstname from ADOXYZ where id=1"); + if ($name == "Carolx") Err("Error: CompleteTrans (2) should have failed"); + else echo "

-- Passed StartTrans test1 - rolling back

"; + } + + $db->StartTrans(); + $db->BeginTrans(); + $db->Execute("update ADOXYZ set firstname='Carolx' where id=1"); + $db->RollbackTrans(); + $rez = $db->CompleteTrans(); + if ($rez !== true) Err("Error: CompleteTrans (1) should have succeeded"); + else { + $name = $db->GetOne("Select firstname from ADOXYZ where id=1"); + if (trim($name) != "Carolx") Err("Error: CompleteTrans (2) should have succeeded, returned name=$name"); + else echo "

-- Passed StartTrans test2 - commiting

"; + } + } + flush(); + $saved = $db->debug; + $db->debug=1; + $cnt = _adodb_getcount($db, 'select * from ADOXYZ where firstname in (select firstname from ADOXYZ)'); + echo "Count= $cnt"; + $db->debug=$saved; + + global $TESTERRS; + $debugerr = true; + + global $ADODB_LANG;$ADODB_LANG = 'fr'; + $db->debug = false; + $TESTERRS = 0; + $db->raiseErrorFn = 'adodb_test_err'; + global $ERRNO; // from adodb_test_err + $db->Execute('select * from nowhere'); + $metae = $db->MetaError($ERRNO); + if ($metae !== DB_ERROR_NOSUCHTABLE) print "

MetaError=".$metae." wrong, should be ".DB_ERROR_NOSUCHTABLE."

"; + else print "

MetaError ok (".DB_ERROR_NOSUCHTABLE."): ".$db->MetaErrorMsg($metae)."

"; + if ($TESTERRS != 1) print "raiseErrorFn select nowhere failed
"; + $rs = $db->Execute('select * from ADOXYZ'); + if ($debugerr) print " Move"; + $rs->Move(100); + $rs->_queryID = false; + if ($debugerr) print " MoveNext"; + $rs->MoveNext(); + if ($debugerr) print " $rs=false"; + $rs = false; + + flush(); + + print "

SetFetchMode() tests

"; + $db->SetFetchMode(ADODB_FETCH_ASSOC); + $rs = $db->SelectLimit('select firstname from ADOXYZ',1); + if (!isset($rs->fields['firstname'])) Err("BAD FETCH ASSOC"); + + $ADODB_FETCH_MODE = ADODB_FETCH_NUM; + $rs = $db->SelectLimit('select firstname from ADOXYZ',1); + //var_dump($rs->fields); + if (!isset($rs->fields['firstname'])) Err("BAD FETCH ASSOC"); + + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $db->SetFetchMode(ADODB_FETCH_NUM); + $rs = $db->SelectLimit('select firstname from ADOXYZ',1); + if (!isset($rs->fields[0])) Err("BAD FETCH NUM"); + + flush(); + + print "

Test MetaTables again with SetFetchMode()

"; + $ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + $db->SetFetchMode(ADODB_FETCH_ASSOC); + print_r($db->MetaTables()); + print "

"; + + //////////////////////////////////////////////////////////////////// + + print "

Testing Bad Connection

"; + flush(); + + if (true || PHP_VERSION < 5) { + if ($db->dataProvider == 'odbtp') $db->databaseType = 'odbtp'; + $conn = NewADOConnection($db->databaseType); + $conn->raiseErrorFn = 'adodb_test_err'; + if (1) $conn->PConnect('abc','baduser','badpassword'); + if ($TESTERRS == 2) print "raiseErrorFn tests passed
"; + else print "raiseErrorFn tests failed ($TESTERRS)
"; + + flush(); + } + //////////////////////////////////////////////////////////////////// + + global $nocountrecs; + + if (isset($nocountrecs) && $ADODB_COUNTRECS) err("Error: \$ADODB_COUNTRECS is set"); + if (empty($nocountrecs) && $ADODB_COUNTRECS==false) err("Error: \$ADODB_COUNTRECS is not set"); + + flush(); +?> +

+
 
+

+Close(); + if ($rs2) $rs2->Close(); + if ($rs) $rs->Close(); + $db->Close(); + + if ($db->transCnt != 0) Err("Error in transCnt=$db->transCnt (should be 0)"); + + + printf("

Total queries=%d; total cached=%d

",$EXECS+$CACHED, $CACHED); + flush(); +} + +function adodb_test_err($dbms, $fn, $errno, $errmsg, $p1=false, $p2=false) +{ +global $TESTERRS,$ERRNO; + + $ERRNO = $errno; + $TESTERRS += 1; + print "** $dbms ($fn): errno=$errno   errmsg=$errmsg ($p1,$p2)
"; +} + +//-------------------------------------------------------------------------------------- + + +@set_time_limit(240); // increase timeout + +include("../tohtml.inc.php"); +include("../adodb.inc.php"); +include("../rsfilter.inc.php"); + +/* White Space Check */ + +if (isset($_SERVER['argv'][1])) { + //print_r($_SERVER['argv']); + $_GET[$_SERVER['argv'][1]] = 1; +} + +if (@$_SERVER['COMPUTERNAME'] == 'TIGRESS') { + CheckWS('mysqlt'); + CheckWS('postgres'); + CheckWS('oci8po'); + + CheckWS('firebird'); + CheckWS('sybase'); + if (!ini_get('safe_mode')) CheckWS('informix'); + + CheckWS('ado_mssql'); + CheckWS('ado_access'); + CheckWS('mssql'); + + CheckWS('vfp'); + CheckWS('sqlanywhere'); + CheckWS('db2'); + CheckWS('access'); + CheckWS('odbc_mssql'); + CheckWS('firebird15'); + // + CheckWS('oracle'); + CheckWS('proxy'); + CheckWS('fbsql'); + print "White Space Check complete

"; +} +if (sizeof($_GET) == 0) $testmysql = true; + + +foreach($_GET as $k=>$v) { + // XSS protection (see Github issue #274) - only set variables for + // expected get parameters used in testdatabases.inc.php + if(preg_match('/^(test|no)\w+$/', $k)) { + $$k = $v; + } +} + +?> + +ADODB Testing + +

ADODB Test

+ +This script tests the following databases: Interbase, Oracle, Visual FoxPro, Microsoft Access (ODBC and ADO), MySQL, MSSQL (ODBC, native, ADO). +There is also support for Sybase, PostgreSQL.

+For the latest version of ADODB, visit
adodb.sourceforge.net.

+ +Test GetInsertSQL/GetUpdateSQL   + Sessions   + Paging   + Perf Monitor

+vers=",ADOConnection::Version(); + + + +?> +

ADODB Database Library (c) 2000-2014 John Lim. All rights reserved. Released under BSD and LGPL, PHP .

+ + diff --git a/app/vendor/adodb/adodb-php/tests/test2.php b/app/vendor/adodb/adodb-php/tests/test2.php new file mode 100644 index 000000000..eb3b02582 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test2.php @@ -0,0 +1,25 @@ +debug=1; + $access = 'd:\inetpub\wwwroot\php\NWIND.MDB'; + $myDSN = 'PROVIDER=Microsoft.Jet.OLEDB.4.0;' + . 'DATA SOURCE=' . $access . ';'; + + echo "

PHP ",PHP_VERSION,"

"; + + $db->Connect($myDSN) || die('fail'); + + print_r($db->ServerInfo()); + + try { + $rs = $db->Execute("select $db->sysTimeStamp,* from adoxyz where id>02xx"); + print_r($rs->fields); + } catch(exception $e) { + print_r($e); + echo "

Date m/d/Y =",$db->UserDate($rs->fields[4],'m/d/Y'); + } diff --git a/app/vendor/adodb/adodb-php/tests/test3.php b/app/vendor/adodb/adodb-php/tests/test3.php new file mode 100644 index 000000000..78c7c775a --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test3.php @@ -0,0 +1,44 @@ +Connect('','scott','natsoft'); +$db->debug=1; + +$cnt = $db->GetOne("select count(*) from adoxyz"); +$rs = $db->Execute("select * from adoxyz order by id"); + +$i = 0; +foreach($rs as $k => $v) { + $i += 1; + echo $k; adodb_pr($v); + flush(); +} + +if ($i != $cnt) die("actual cnt is $i, cnt should be $cnt\n"); + + + +$rs = $db->Execute("select bad from badder"); + +} catch (exception $e) { + adodb_pr($e); + $e = adodb_backtrace($e->trace); +} diff --git a/app/vendor/adodb/adodb-php/tests/test4.php b/app/vendor/adodb/adodb-php/tests/test4.php new file mode 100644 index 000000000..7a5d821ca --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test4.php @@ -0,0 +1,144 @@ +PConnect("", "sa", "natsoft", "northwind"); // connect to MySQL, testdb + +$conn = ADONewConnection("mysql"); // create a connection +$conn->PConnect("localhost", "root", "", "test"); // connect to MySQL, testdb + + +#$conn = ADONewConnection('oci8po'); +#$conn->Connect('','scott','natsoft'); + +if (PHP_VERSION >= 5) { + $connstr = "mysql:dbname=northwind"; + $u = 'root';$p=''; + $conn = ADONewConnection('pdo'); + $conn->Connect($connstr, $u, $p); +} +//$ADODB_FETCH_MODE = ADODB_FETCH_ASSOC; + + +$conn->debug=1; +$conn->Execute("delete from adoxyz where lastname like 'Smi%'"); + +$rs = $conn->Execute($sql); // Execute the query and get the empty recordset +$record = array(); // Initialize an array to hold the record data to insert + +if (strpos($conn->databaseType,'mysql')===false) $record['id'] = 751; +$record["firstname"] = 'Jann'; +$record["lastname"] = "Smitts"; +$record["created"] = time(); + +$insertSQL = $conn->GetInsertSQL($rs, $record); +$conn->Execute($insertSQL); // Insert the record into the database + +if (strpos($conn->databaseType,'mysql')===false) $record['id'] = 752; +// Set the values for the fields in the record +$record["firstname"] = 'anull'; +$record["lastname"] = "Smith\$@//"; +$record["created"] = time(); + +if (isset($_GET['f'])) $ADODB_FORCE_TYPE = $_GET['f']; + +//$record["id"] = -1; + +// Pass the empty recordset and the array containing the data to insert +// into the GetInsertSQL function. The function will process the data and return +// a fully formatted insert sql statement. +$insertSQL = $conn->GetInsertSQL($rs, $record); +$conn->Execute($insertSQL); // Insert the record into the database + + + +$insertSQL2 = $conn->GetInsertSQL($table='ADOXYZ', $record); +if ($insertSQL != $insertSQL2) echo "

Walt's new stuff failed: $insertSQL2

"; +//========================== +// This code tests an update + +$sql = " +SELECT * +FROM ADOXYZ WHERE lastname=".$conn->Param('var'). " ORDER BY 1"; +// Select a record to update + +$varr = array('var'=>$record['lastname'].''); +$rs = $conn->Execute($sql,$varr); // Execute the query and get the existing record to update +if (!$rs || $rs->EOF) print "

No record found!

"; + +$record = array(); // Initialize an array to hold the record data to update + + +// Set the values for the fields in the record +$record["firstName"] = "Caroline".rand(); +//$record["lasTname"] = ""; // Update Caroline's lastname from Miranda to Smith +$record["creAted"] = '2002-12-'.(rand()%30+1); +$record['num'] = ''; +// Pass the single record recordset and the array containing the data to update +// into the GetUpdateSQL function. The function will process the data and return +// a fully formatted update sql statement. +// If the data has not changed, no recordset is returned + +$updateSQL = $conn->GetUpdateSQL($rs, $record); +$conn->Execute($updateSQL,$varr); // Update the record in the database +if ($conn->Affected_Rows() != 1)print "

Error1 : Rows Affected=".$conn->Affected_Rows().", should be 1

"; + +$record["firstName"] = "Caroline".rand(); +$record["lasTname"] = "Smithy Jones"; // Update Caroline's lastname from Miranda to Smith +$record["creAted"] = '2002-12-'.(rand()%30+1); +$record['num'] = 331; +$updateSQL = $conn->GetUpdateSQL($rs, $record); +$conn->Execute($updateSQL,$varr); // Update the record in the database +if ($conn->Affected_Rows() != 1)print "

Error 2: Rows Affected=".$conn->Affected_Rows().", should be 1

"; + +$rs = $conn->Execute("select * from ADOXYZ where lastname like 'Sm%'"); +//adodb_pr($rs); +rs2html($rs); + +$record["firstName"] = "Carol-new-".rand(); +$record["lasTname"] = "Smithy"; // Update Caroline's lastname from Miranda to Smith +$record["creAted"] = '2002-12-'.(rand()%30+1); +$record['num'] = 331; + +$conn->AutoExecute('ADOXYZ',$record,'UPDATE', "lastname like 'Sm%'"); +$rs = $conn->Execute("select * from ADOXYZ where lastname like 'Sm%'"); +//adodb_pr($rs); +rs2html($rs); +} + + +testsql(); diff --git a/app/vendor/adodb/adodb-php/tests/test5.php b/app/vendor/adodb/adodb-php/tests/test5.php new file mode 100644 index 000000000..e46bbc1df --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test5.php @@ -0,0 +1,48 @@ +debug=1; + $conn->PConnect("localhost","root","","xphplens"); + print $conn->databaseType.':'.$conn->GenID().'
'; +} + +if (0) { + $conn = ADONewConnection("oci8"); // create a connection + $conn->debug=1; + $conn->PConnect("falcon", "scott", "tiger", "juris8.ecosystem.natsoft.com.my"); // connect to MySQL, testdb + print $conn->databaseType.':'.$conn->GenID(); +} + +if (0) { + $conn = ADONewConnection("ibase"); // create a connection + $conn->debug=1; + $conn->Connect("localhost:c:\\Interbase\\Examples\\Database\\employee.gdb", "sysdba", "masterkey", ""); // connect to MySQL, testdb + print $conn->databaseType.':'.$conn->GenID().'
'; +} + +if (0) { + $conn = ADONewConnection('postgres'); + $conn->debug=1; + @$conn->PConnect("susetikus","tester","test","test"); + print $conn->databaseType.':'.$conn->GenID().'
'; +} diff --git a/app/vendor/adodb/adodb-php/tests/test_rs_array.php b/app/vendor/adodb/adodb-php/tests/test_rs_array.php new file mode 100644 index 000000000..547b20ad6 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/test_rs_array.php @@ -0,0 +1,46 @@ +InitArray($array,$typearr); + +while (!$rs->EOF) { + print_r($rs->fields);echo "
"; + $rs->MoveNext(); +} + +echo "
1 Seek
"; +$rs->Move(1); +while (!$rs->EOF) { + print_r($rs->fields);echo "
"; + $rs->MoveNext(); +} + +echo "
2 Seek
"; +$rs->Move(2); +while (!$rs->EOF) { + print_r($rs->fields);echo "
"; + $rs->MoveNext(); +} + +echo "
3 Seek
"; +$rs->Move(3); +while (!$rs->EOF) { + print_r($rs->fields);echo "
"; + $rs->MoveNext(); +} + + + +die(); diff --git a/app/vendor/adodb/adodb-php/tests/testcache.php b/app/vendor/adodb/adodb-php/tests/testcache.php new file mode 100644 index 000000000..87f6d51a1 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/testcache.php @@ -0,0 +1,30 @@ + + +PConnect('nwind'); +} else { + $db = ADONewConnection('mysql'); + $db->PConnect('mangrove','root','','xphplens'); +} +if (isset($cache)) $rs = $db->CacheExecute(120,'select * from products'); +else $rs = $db->Execute('select * from products'); + +$arr = $rs->GetArray(); +print sizeof($arr); diff --git a/app/vendor/adodb/adodb-php/tests/testdatabases.inc.php b/app/vendor/adodb/adodb-php/tests/testdatabases.inc.php new file mode 100644 index 000000000..f9f000a64 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/testdatabases.inc.php @@ -0,0 +1,478 @@ + + +
+
+> Access
+> Interbase
+> MSSQL
+> MySQL
+> MySQL ODBC
+> MySQLi +
+
> SQLite
+> MySQL Proxy
+> Oracle (oci8)
+> PostgreSQL
+> PostgreSQL 9
+> PostgreSQL ODBC
+
+> PgSQL PDO
+> MySQL PDO
+> SQLite PDO
+> Access PDO
+> MSSQL PDO
+ +> OCI PDO
+ +
> DB2
+> VFP+ODBTP
+> ADO (for mssql and access)
+> $ADODB_COUNTRECS=false
+> No SQL Logging
+> ADOdb time test +
+ + + +FETCH MODE IS NOT ADODB_FETCH_DEFAULT"; + +if (isset($nocountrecs)) $ADODB_COUNTRECS = false; + +// cannot test databases below, but we include them anyway to check +// if they parse ok... + +if (sizeof($_GET) || !isset($_SERVER['HTTP_HOST'])) { + echo "
"; + ADOLoadCode2("sybase"); + ADOLoadCode2("postgres"); + ADOLoadCode2("postgres7"); + ADOLoadCode2("firebird"); + ADOLoadCode2("borland_ibase"); + ADOLoadCode2("informix"); + ADOLoadCode2('mysqli'); + if (defined('ODBC_BINMODE_RETURN')) { + ADOLoadCode2("sqlanywhere"); + ADOLoadCode2("access"); + } + ADOLoadCode2("mysql"); + ADOLoadCode2("oci8"); +} + +function ADOLoadCode2($d) +{ + ADOLoadCode($d); + $c = ADONewConnection($d); + echo "Loaded $d ",($c ? 'ok' : 'extension not installed'),"
"; +} + +flush(); + +// dregad 2014-04-15 added serial field to avoid error with lastval() +$pg_test_table = "create table ADOXYZ (id integer, firstname char(24), lastname varchar,created date, ser serial)"; +$pg_hostname = 'localhost'; +$pg_user = 'tester'; +$pg_password = 'test'; +$pg_database = 'northwind'; +$pg_errmsg = "ERROR: PostgreSQL requires a database called '$pg_database' " + . "on server '$pg_hostname', user '$pg_user', password '$pg_password'.
"; + +if (!empty($testpostgres)) { + //ADOLoadCode("postgres"); + + $db = ADONewConnection('postgres'); + print "

Connecting $db->databaseType...

"; + if ($db->Connect($pg_hostname, $pg_user, $pg_password, $pg_database)) { + testdb($db, $pg_test_table); + } else { + print $pg_errmsg . $db->ErrorMsg(); + } +} + +if (!empty($testpostgres9)) { + //ADOLoadCode("postgres"); + + $db = ADONewConnection('postgres9'); + print "

Connecting $db->databaseType...

"; + if ($db->Connect($pg_hostname, $pg_user, $pg_password, $pg_database)) { + testdb($db, $pg_test_table); + } else { + print $pg_errmsg . $db->ErrorMsg(); + } +} + +if (!empty($testpgodbc)) { + + $db = ADONewConnection('odbc'); + $db->hasTransactions = false; + print "

Connecting $db->databaseType...

"; + + if ($db->PConnect('Postgresql')) { + $db->hasTransactions = true; + testdb($db, + "create table ADOXYZ (id int, firstname char(24), lastname char(24), created date) type=innodb"); + } else print "ERROR: PostgreSQL requires a database called test on server, user tester, password test.
".$db->ErrorMsg(); +} + +if (!empty($testibase)) { + //$_GET['nolog'] = true; + $db = ADONewConnection('firebird'); + print "

Connecting $db->databaseType...

"; + if ($db->PConnect("localhost:d:\\firebird\\151\\examples\\EMPLOYEE.fdb", "sysdba", "masterkey", "")) + testdb($db,"create table ADOXYZ (id integer, firstname char(24), lastname char(24),price numeric(12,2),created date)"); + else print "ERROR: Interbase test requires a database called employee.gdb".'
'.$db->ErrorMsg(); + +} + + +if (!empty($testsqlite)) { + $path =urlencode('d:\inetpub\adodb\sqlite.db'); + $dsn = "sqlite://$path/"; + $db = ADONewConnection($dsn); + //echo $dsn; + + //$db = ADONewConnection('sqlite'); + + + if ($db && $db->PConnect("d:\\inetpub\\adodb\\sqlite.db", "", "", "")) { + print "

Connecting $db->databaseType...

"; + testdb($db,"create table ADOXYZ (id int, firstname char(24), lastname char(24),created datetime)"); + } else + print "ERROR: SQLite"; + +} + +if (!empty($testpdopgsql)) { + $connstr = "pgsql:dbname=test"; + $u = 'tester';$p='test'; + $db = ADONewConnection('pdo'); + print "

Connecting $db->databaseType...

"; + $db->Connect($connstr,$u,$p) || die("CONNECT FAILED"); + testdb($db, + "create table ADOXYZ (id int, firstname char(24), lastname char(24), created date)"); +} + +if (!empty($testpdomysql)) { + $connstr = "mysql:dbname=northwind"; + $u = 'root';$p=''; + $db = ADONewConnection('pdo'); + print "

Connecting $db->databaseType...

"; + $db->Connect($connstr,$u,$p) || die("CONNECT FAILED"); + + testdb($db, + "create table ADOXYZ (id int, firstname char(24), lastname char(24), created date)"); +} + +if (!empty($testpdomssql)) { + $connstr = "mssql:dbname=northwind"; + $u = 'sa';$p='natsoft'; + $db = ADONewConnection('pdo'); + print "

Connecting $db->databaseType...

"; + $db->Connect($connstr,$u,$p) || die("CONNECT FAILED"); + + testdb($db, + "create table ADOXYZ (id int, firstname char(24), lastname char(24), created date)"); +} + +if (!empty($testpdosqlite)) { + $connstr = "sqlite:d:/inetpub/adodb/sqlite-pdo.db3"; + $u = '';$p=''; + $db = ADONewConnection('pdo'); + $db->hasTransactions = false; + print "

Connecting $db->databaseType...

"; + $db->Connect($connstr,$u,$p) || die("CONNECT FAILED"); + testdb($db, + "create table ADOXYZ (id int, firstname char(24), lastname char(24), created date)"); +} + +if (!empty($testpdoaccess)) { + $connstr = 'odbc:nwind'; + $u = '';$p=''; + $db = ADONewConnection('pdo'); + $db->hasTransactions = false; + print "

Connecting $db->databaseType...

"; + $db->Connect($connstr,$u,$p) || die("CONNECT FAILED"); + testdb($db, + "create table ADOXYZ (id int, firstname char(24), lastname char(24), created date)"); +} + +if (!empty($testpdoora)) { + $connstr = 'oci:'; + $u = 'scott';$p='natsoft'; + $db = ADONewConnection('pdo'); + #$db->hasTransactions = false; + print "

Connecting $db->databaseType...

"; + $db->Connect($connstr,$u,$p) || die("CONNECT FAILED"); + testdb($db, + "create table ADOXYZ (id int, firstname char(24), lastname char(24), created date)"); +} + +// REQUIRES ODBC DSN CALLED nwind +if (!empty($testaccess)) { + $db = ADONewConnection('access'); + print "

Connecting $db->databaseType...

"; + $access = 'd:\inetpub\wwwroot\php\NWIND.MDB'; + $dsn = "nwind"; + $dsn = "Driver={Microsoft Access Driver (*.mdb)};Dbq=$access;Uid=Admin;Pwd=;"; + + //$dsn = 'Provider=Microsoft.Jet.OLEDB.4.0;DATA SOURCE=' . $access . ';'; + if ($db->PConnect($dsn, "", "", "")) + testdb($db,"create table ADOXYZ (id int, firstname char(24), lastname char(24),created datetime)"); + else print "ERROR: Access test requires a Windows ODBC DSN=nwind, Access driver"; + +} + +if (!empty($testaccess) && !empty($testado)) { // ADO ACCESS + + $db = ADONewConnection("ado_access"); + print "

Connecting $db->databaseType...

"; + + $access = 'd:\inetpub\wwwroot\php\NWIND.MDB'; + $myDSN = 'PROVIDER=Microsoft.Jet.OLEDB.4.0;' + . 'DATA SOURCE=' . $access . ';'; + //. 'USER ID=;PASSWORD=;'; + $_GET['nolog'] = 1; + if ($db->PConnect($myDSN, "", "", "")) { + print "ADO version=".$db->_connectionID->version."
"; + testdb($db,"create table ADOXYZ (id int, firstname char(24), lastname char(24),created datetime)"); + } else print "ERROR: Access test requires a Access database $access".'
'.$db->ErrorMsg(); + +} + +if (!empty($testvfp)) { // ODBC + $db = ADONewConnection('vfp'); + print "

Connecting $db->databaseType...

";flush(); + + if ( $db->PConnect("vfp-adoxyz")) { + testdb($db,"create table d:\\inetpub\\adodb\\ADOXYZ (id int, firstname char(24), lastname char(24),created date)"); + } else print "ERROR: Visual FoxPro test requires a Windows ODBC DSN=vfp-adoxyz, VFP driver"; + + echo "
"; + $db = ADONewConnection('odbtp'); + + if ( $db->PConnect('localhost','DRIVER={Microsoft Visual FoxPro Driver};SOURCETYPE=DBF;SOURCEDB=d:\inetpub\adodb;EXCLUSIVE=NO;')) { + print "

Connecting $db->databaseType...

";flush(); + testdb($db,"create table d:\\inetpub\\adodb\\ADOXYZ (id int, firstname char(24), lastname char(24),created date)"); + } else print "ERROR: Visual FoxPro odbtp requires a Windows ODBC DSN=vfp-adoxyz, VFP driver"; + +} + + +// REQUIRES MySQL server at localhost with database 'test' +if (!empty($testmysql)) { // MYSQL + + + if (PHP_VERSION >= 5 || $_SERVER['HTTP_HOST'] == 'localhost') $server = 'localhost'; + else $server = "mangrove"; + $user = 'root'; $password = ''; $database = 'northwind'; + $db = ADONewConnection("mysqlt://$user:$password@$server/$database?persist"); + print "

Connecting $db->databaseType...

"; + + if (true || $db->PConnect($server, "root", "", "northwind")) { + //$db->Execute("DROP TABLE ADOXYZ") || die('fail drop'); + //$db->debug=1;$db->Execute('drop table ADOXYZ'); + testdb($db, + "create table ADOXYZ (id int, firstname char(24), lastname char(24), created date) Type=InnoDB"); + } else print "ERROR: MySQL test requires a MySQL server on localhost, userid='admin', password='', database='test'".'
'.$db->ErrorMsg(); +} + +// REQUIRES MySQL server at localhost with database 'test' +if (!empty($testmysqli)) { // MYSQL + + $db = ADONewConnection('mysqli'); + print "

Connecting $db->databaseType...

"; + if (PHP_VERSION >= 5 || $_SERVER['HTTP_HOST'] == 'localhost') $server = 'localhost'; + else $server = "mangrove"; + if ($db->PConnect($server, "root", "", "northwind")) { + //$db->debug=1;$db->Execute('drop table ADOXYZ'); + testdb($db, + "create table ADOXYZ (id int, firstname char(24), lastname char(24), created date)"); + } else print "ERROR: MySQL test requires a MySQL server on localhost, userid='admin', password='', database='test'".'
'.$db->ErrorMsg(); +} + + +// REQUIRES MySQL server at localhost with database 'test' +if (!empty($testmysqlodbc)) { // MYSQL + + $db = ADONewConnection('odbc'); + $db->hasTransactions = false; + print "

Connecting $db->databaseType...

"; + if ($_SERVER['HTTP_HOST'] == 'localhost') $server = 'localhost'; + else $server = "mangrove"; + if ($db->PConnect('mysql', "root", "")) + testdb($db, + "create table ADOXYZ (id int, firstname char(24), lastname char(24), created date) type=innodb"); + else print "ERROR: MySQL test requires a MySQL server on localhost, userid='admin', password='', database='test'".'
'.$db->ErrorMsg(); +} + +if (!empty($testproxy)){ + $db = ADONewConnection('proxy'); + print "

Connecting $db->databaseType...

"; + if ($_SERVER['HTTP_HOST'] == 'localhost') $server = 'localhost'; + + if ($db->PConnect('http://localhost/php/phplens/adodb/server.php')) + testdb($db, + "create table ADOXYZ (id int, firstname char(24), lastname char(24), created date) type=innodb"); + else print "ERROR: MySQL test requires a MySQL server on localhost, userid='admin', password='', database='test'".'
'.$db->ErrorMsg(); + +} + +ADOLoadCode('oci805'); +ADOLoadCode("oci8po"); + +if (!empty($testoracle)) { + $dsn = "oci8";//://scott:natsoft@kk2?persist"; + $db = ADONewConnection($dsn );//'oci8'); + + //$db->debug=1; + print "

Connecting $db->databaseType...

"; + if ($db->Connect('mobydick', "scott", "natsoft",'SID=mobydick')) + testdb($db,"create table ADOXYZ (id int, firstname varchar(24), lastname varchar(24),created date)"); + else + print "ERROR: Oracle test requires an Oracle server setup with scott/natsoft".'
'.$db->ErrorMsg(); + +} +ADOLoadCode("oracle"); // no longer supported +if (false && !empty($testoracle)) { + + $db = ADONewConnection(); + print "

Connecting $db->databaseType...

"; + if ($db->PConnect("", "scott", "tiger", "natsoft.domain")) + testdb($db,"create table ADOXYZ (id int, firstname varchar(24), lastname varchar(24),created date)"); + else print "ERROR: Oracle test requires an Oracle server setup with scott/tiger".'
'.$db->ErrorMsg(); + +} + +ADOLoadCode("odbc_db2"); // no longer supported +if (!empty($testdb2)) { + if (PHP_VERSION>=5.1) { + $db = ADONewConnection("db2"); + print "

Connecting $db->databaseType...

"; + + #$db->curMode = SQL_CUR_USE_ODBC; + #$dsn = "driver={IBM db2 odbc DRIVER};Database=test;hostname=localhost;port=50000;protocol=TCPIP; uid=natsoft; pwd=guest"; + if ($db->Connect('localhost','natsoft','guest','test')) { + testdb($db,"create table ADOXYZ (id int, firstname varchar(24), lastname varchar(24),created date)"); + } else print "ERROR: DB2 test requires an server setup with odbc data source db2_sample".'
'.$db->ErrorMsg(); + } else { + $db = ADONewConnection("odbc_db2"); + print "

Connecting $db->databaseType...

"; + + $dsn = "db2test"; + #$db->curMode = SQL_CUR_USE_ODBC; + #$dsn = "driver={IBM db2 odbc DRIVER};Database=test;hostname=localhost;port=50000;protocol=TCPIP; uid=natsoft; pwd=guest"; + if ($db->Connect($dsn)) { + testdb($db,"create table ADOXYZ (id int, firstname varchar(24), lastname varchar(24),created date)"); + } else print "ERROR: DB2 test requires an server setup with odbc data source db2_sample".'
'.$db->ErrorMsg(); + } +echo "
"; +flush(); + $dsn = "driver={IBM db2 odbc DRIVER};Database=sample;hostname=localhost;port=50000;protocol=TCPIP; uid=root; pwd=natsoft"; + + $db = ADONewConnection('odbtp'); + if ($db->Connect('127.0.0.1',$dsn)) { + + $db->debug=1; + $arr = $db->GetArray( "||SQLProcedures" ); adodb_pr($arr); + $arr = $db->GetArray( "||SQLProcedureColumns|||GET_ROUTINE_SAR" );adodb_pr($arr); + + testdb($db,"create table ADOXYZ (id int, firstname varchar(24), lastname varchar(24),created date)"); + } else echo ("ERROR Connection"); + echo $db->ErrorMsg(); +} + + +$server = 'localhost'; + + + +ADOLoadCode("mssqlpo"); +if (false && !empty($testmssql)) { // MS SQL Server -- the extension is buggy -- probably better to use ODBC + $db = ADONewConnection("mssqlpo"); + //$db->debug=1; + print "

Connecting $db->databaseType...

"; + + $ok = $db->Connect('','sa','natsoft','northwind'); + echo $db->ErrorMsg(); + if ($ok /*or $db->PConnect("mangrove", "sa", "natsoft", "ai")*/) { + AutoDetect_MSSQL_Date_Order($db); + // $db->Execute('drop table adoxyz'); + testdb($db,"create table ADOXYZ (id int, firstname char(24) null, lastname char(24) null,created datetime null)"); + } else print "ERROR: MSSQL test 2 requires a MS SQL 7 on a server='$server', userid='adodb', password='natsoft', database='ai'".'
'.$db->ErrorMsg(); + +} + + +ADOLoadCode('odbc_mssql'); +if (!empty($testmssql)) { // MS SQL Server via ODBC + $db = ADONewConnection(); + + print "

Connecting $db->databaseType...

"; + + $dsn = "PROVIDER=MSDASQL;Driver={SQL Server};Server=$server;Database=northwind;"; + $dsn = 'condor'; + if ($db->PConnect($dsn, "sa", "natsoft", "")) { + testdb($db,"create table ADOXYZ (id int, firstname char(24) null, lastname char(24) null,created datetime null)"); + } + else print "ERROR: MSSQL test 1 requires a MS SQL 7 server setup with DSN setup"; + +} + +ADOLoadCode("ado_mssql"); +if (!empty($testmssql) && !empty($testado) ) { // ADO ACCESS MSSQL -- thru ODBC -- DSN-less + + $db = ADONewConnection("ado_mssql"); + //$db->debug=1; + print "

Connecting DSN-less $db->databaseType...

"; + + $myDSN="PROVIDER=MSDASQL;DRIVER={SQL Server};" + . "SERVER=$server;DATABASE=NorthWind;UID=adodb;PWD=natsoft;Trusted_Connection=No"; + + + if ($db->PConnect($myDSN, "", "", "")) + testdb($db,"create table ADOXYZ (id int, firstname char(24) null, lastname char(24) null,created datetime null)"); + else print "ERROR: MSSQL test 2 requires MS SQL 7"; + +} + +if (!empty($testmssql) && !empty($testado)) { // ADO ACCESS MSSQL with OLEDB provider + + $db = ADONewConnection("ado_mssql"); + print "

Connecting DSN-less OLEDB Provider $db->databaseType...

"; + //$db->debug=1; + $myDSN="SERVER=localhost;DATABASE=northwind;Trusted_Connection=yes"; + if ($db->PConnect($myDSN, "adodb", "natsoft", 'SQLOLEDB')) { + testdb($db,"create table ADOXYZ (id int, firstname char(24), lastname char(24),created datetime)"); + } else print "ERROR: MSSQL test 2 requires a MS SQL 7 on a server='mangrove', userid='sa', password='', database='ai'"; + +} + + +if (extension_loaded('odbtp') && !empty($testmssql)) { // MS SQL Server via ODBC + $db = ADONewConnection('odbtp'); + + $dsn = "PROVIDER=MSDASQL;Driver={SQL Server};Server=$server;Database=northwind;uid=adodb;pwd=natsoft"; + + if ($db->PConnect('localhost',$dsn, "", "")) { + print "

Connecting $db->databaseType...

"; + testdb($db,"create table ADOXYZ (id int, firstname char(24) null, lastname char(24) null,created datetime null)"); + } + else print "ERROR: MSSQL test 1 requires a MS SQL 7 server setup with DSN setup"; + +} + + +print "

Tests Completed

"; diff --git a/app/vendor/adodb/adodb-php/tests/testgenid.php b/app/vendor/adodb/adodb-php/tests/testgenid.php new file mode 100644 index 000000000..3310734a5 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/testgenid.php @@ -0,0 +1,35 @@ +Execute("drop table $table"); + //$db->debug=true; + + $ctr = 5000; + $lastnum = 0; + + while (--$ctr >= 0) { + $num = $db->GenID($table); + if ($num === false) { + print "GenID returned false"; + break; + } + if ($lastnum + 1 == $num) print " $num "; + else { + print " $num "; + flush(); + } + $lastnum = $num; + } +} diff --git a/app/vendor/adodb/adodb-php/tests/testmssql.php b/app/vendor/adodb/adodb-php/tests/testmssql.php new file mode 100644 index 000000000..733f0d455 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/testmssql.php @@ -0,0 +1,77 @@ +Connect('127.0.0.1','adodb','natsoft','northwind') or die('Fail'); + +$conn->debug =1; +$query = 'select * from products'; +$conn->SetFetchMode(ADODB_FETCH_ASSOC); +$rs = $conn->Execute($query); +echo "
";
+while( !$rs->EOF ) {
+	$output[] = $rs->fields;
+	var_dump($rs->fields);
+	$rs->MoveNext();
+	print "

"; +} +die(); + + +$p = $conn->Prepare('insert into products (productname,unitprice,dcreated) values (?,?,?)'); +echo "

";
+print_r($p);
+
+$conn->debug=1;
+$conn->Execute($p,array('John'.rand(),33.3,$conn->DBDate(time())));
+
+$p = $conn->Prepare('select * from products where productname like ?');
+$arr = $conn->getarray($p,array('V%'));
+print_r($arr);
+die();
+
+//$conn = ADONewConnection("mssql");
+//$conn->Connect('mangrove','sa','natsoft','ai');
+
+//$conn->Connect('mangrove','sa','natsoft','ai');
+$conn->debug=1;
+$conn->Execute('delete from blobtest');
+
+$conn->Execute('insert into blobtest (id) values(1)');
+$conn->UpdateBlobFile('blobtest','b1','../cute_icons_for_site/adodb.gif','id=1');
+$rs = $conn->Execute('select b1 from blobtest where id=1');
+
+$output = "c:\\temp\\test_out-".date('H-i-s').".gif";
+print "Saving file $output, size=".strlen($rs->fields[0])."

"; +$fd = fopen($output, "wb"); +fwrite($fd, $rs->fields[0]); +fclose($fd); + +print " View Image"; +//$rs = $conn->Execute('SELECT id,SUBSTRING(b1, 1, 10) FROM blobtest'); +//rs2html($rs); diff --git a/app/vendor/adodb/adodb-php/tests/testoci8.php b/app/vendor/adodb/adodb-php/tests/testoci8.php new file mode 100644 index 000000000..5eadc22ed --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/testoci8.php @@ -0,0 +1,84 @@ + + +PConnect('','scott','natsoft'); + if (!empty($testblob)) { + $varHoldingBlob = 'ABC DEF GEF John TEST'; + $num = time()%10240; + // create table atable (id integer, ablob blob); + $db->Execute('insert into ATABLE (id,ablob) values('.$num.',empty_blob())'); + $db->UpdateBlob('ATABLE', 'ablob', $varHoldingBlob, 'id='.$num, 'BLOB'); + + $rs = $db->Execute('select * from atable'); + + if (!$rs) die("Empty RS"); + if ($rs->EOF) die("EOF RS"); + rs2html($rs); + } + $stmt = $db->Prepare('select * from adoxyz where id=?'); + for ($i = 1; $i <= 10; $i++) { + $rs = $db->Execute( + $stmt, + array($i)); + + if (!$rs) die("Empty RS"); + if ($rs->EOF) die("EOF RS"); + rs2html($rs); + } +} +if (1) { + $db = ADONewConnection('oci8'); + $db->PConnect('','scott','natsoft'); + $db->debug = true; + $db->Execute("delete from emp where ename='John'"); + print $db->Affected_Rows().'
'; + $stmt = $db->Prepare('insert into emp (empno, ename) values (:empno, :ename)'); + $rs = $db->Execute($stmt,array('empno'=>4321,'ename'=>'John')); + // prepare not quite ready for prime time + //$rs = $db->Execute($stmt,array('empno'=>3775,'ename'=>'John')); + if (!$rs) die("Empty RS"); + + $db->setfetchmode(ADODB_FETCH_NUM); + + $vv = 'A%'; + $stmt = $db->PrepareSP("BEGIN adodb.open_tab2(:rs,:tt); END;",true); + $db->OutParameter($stmt, $cur, 'rs', -1, OCI_B_CURSOR); + $db->OutParameter($stmt, $vv, 'tt'); + $rs = $db->Execute($stmt); + while (!$rs->EOF) { + adodb_pr($rs->fields); + $rs->MoveNext(); + } + echo " val = $vv"; + +} + +if (0) { + $db = ADONewConnection('odbc_oracle'); + if (!$db->PConnect('local_oracle','scott','tiger')) die('fail connect'); + $db->debug = true; + $rs = $db->Execute( + 'select * from adoxyz where firstname=? and trim(lastname)=?', + array('first'=>'Caroline','last'=>'Miranda')); + if (!$rs) die("Empty RS"); + if ($rs->EOF) die("EOF RS"); + rs2html($rs); +} diff --git a/app/vendor/adodb/adodb-php/tests/testoci8cursor.php b/app/vendor/adodb/adodb-php/tests/testoci8cursor.php new file mode 100644 index 000000000..1f7b381e5 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/testoci8cursor.php @@ -0,0 +1,110 @@ +PConnect('','scott','natsoft'); + $db->debug = 99; + + +/* +*/ + + define('MYNUM',5); + + + $rs = $db->ExecuteCursor("BEGIN adodb.open_tab(:RS,'A%'); END;"); + + if ($rs && !$rs->EOF) { + print "Test 1 RowCount: ".$rs->RecordCount()."

"; + } else { + print "Error in using Cursor Variables 1

"; + } + + print "

Testing Stored Procedures for oci8

"; + + $stid = $db->PrepareSP('BEGIN adodb.myproc('.MYNUM.', :myov); END;'); + $db->OutParameter($stid, $myov, 'myov'); + $db->Execute($stid); + if ($myov != MYNUM) print "

Error with myproc

"; + + + $stmt = $db->PrepareSP("BEGIN adodb.data_out(:a1, :a2); END;",true); + $a1 = 'Malaysia'; + //$a2 = ''; # a2 doesn't even need to be defined! + $db->InParameter($stmt,$a1,'a1'); + $db->OutParameter($stmt,$a2,'a2'); + $rs = $db->Execute($stmt); + if ($rs) { + if ($a2 !== 'Cinta Hati Malaysia') print "Stored Procedure Error: a2 = $a2

"; + else echo "OK: a2=$a2

"; + } else { + print "Error in using Stored Procedure IN/Out Variables

"; + } + + + $tname = 'A%'; + + $stmt = $db->PrepareSP('select * from tab where tname like :tablename'); + $db->Parameter($stmt,$tname,'tablename'); + $rs = $db->Execute($stmt); + rs2html($rs); diff --git a/app/vendor/adodb/adodb-php/tests/testpaging.php b/app/vendor/adodb/adodb-php/tests/testpaging.php new file mode 100644 index 000000000..947641bed --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/testpaging.php @@ -0,0 +1,87 @@ +PConnect('localhost','tester','test','test'); +} + +if ($driver == 'access') { + $db = NewADOConnection('access'); + $db->PConnect("nwind", "", "", ""); +} + +if ($driver == 'ibase') { + $db = NewADOConnection('ibase'); + $db->PConnect("localhost:e:\\firebird\\examples\\employee.gdb", "sysdba", "masterkey", ""); + $sql = 'select distinct firstname, lastname from adoxyz order by firstname'; + +} +if ($driver == 'mssql') { + $db = NewADOConnection('mssql'); + $db->Connect('JAGUAR\vsdotnet','adodb','natsoft','northwind'); +} +if ($driver == 'oci8') { + $db = NewADOConnection('oci8'); + $db->Connect('','scott','natsoft'); + +$sql = "select * from (select ID, firstname as \"First Name\", lastname as \"Last Name\" from adoxyz + order by 1)"; +} + +if ($driver == 'access') { + $db = NewADOConnection('access'); + $db->Connect('nwind'); +} + +if (empty($driver) or $driver == 'mysql') { + $db = NewADOConnection('mysql'); + $db->Connect('localhost','root','','test'); +} + +//$db->pageExecuteCountRows = false; + +$db->debug = true; + +if (0) { +$rs = $db->Execute($sql); +include_once('../toexport.inc.php'); +print "

";
+print rs2csv($rs); # return a string
+
+print '
'; +$rs->MoveFirst(); # note, some databases do not support MoveFirst +print rs2tab($rs); # return a string + +print '
'; +$rs->MoveFirst(); +rs2tabout($rs); # send to stdout directly +print "
"; +} + +$pager = new ADODB_Pager($db,$sql); +$pager->showPageLinks = true; +$pager->linksPerPage = 10; +$pager->cache = 60; +$pager->Render($rows=7); diff --git a/app/vendor/adodb/adodb-php/tests/testpear.php b/app/vendor/adodb/adodb-php/tests/testpear.php new file mode 100644 index 000000000..a59f5f2c6 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/testpear.php @@ -0,0 +1,35 @@ +setFetchMode(ADODB_FETCH_ASSOC); +$rs = $db->Query('select firstname,lastname from adoxyz'); +$cnt = 0; +while ($arr = $rs->FetchRow()) { + print_r($arr); + print "
"; + $cnt += 1; +} + +if ($cnt != 50) print "Error in \$cnt = $cnt"; diff --git a/app/vendor/adodb/adodb-php/tests/testsessions.php b/app/vendor/adodb/adodb-php/tests/testsessions.php new file mode 100644 index 000000000..79a66191c --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/testsessions.php @@ -0,0 +1,100 @@ +Notify Expiring=$ref, sessionkey=$key

"; +} + +//------------------------------------------------------------------- + +error_reporting(E_ALL); + + +ob_start(); +include('../session/adodb-cryptsession2.php'); + + +$options['debug'] = 1; +$db = 'postgres'; + +#### CONNECTION +switch($db) { +case 'oci8': + $options['table'] = 'adodb_sessions2'; + ADOdb_Session::config('oci8', 'mobydick', 'jdev', 'natsoft', 'mobydick',$options); + break; + +case 'postgres': + $options['table'] = 'sessions2'; + ADOdb_Session::config('postgres', 'localhost', 'postgres', 'natsoft', 'northwind',$options); + break; + +case 'mysql': +default: + $options['table'] = 'sessions2'; + ADOdb_Session::config('mysql', 'localhost', 'root', '', 'xphplens_2',$options); + break; + + +} + + + +#### SETUP NOTIFICATION + $USER = 'JLIM'.rand(); + $ADODB_SESSION_EXPIRE_NOTIFY = array('USER','NotifyExpire'); + + adodb_session_create_table(); + session_start(); + + adodb_session_regenerate_id(); + +### SETUP SESSION VARIABLES + if (empty($_SESSION['MONKEY'])) $_SESSION['MONKEY'] = array(1,'abc',44.41); + else $_SESSION['MONKEY'][0] += 1; + if (!isset($_GET['nochange'])) @$_SESSION['AVAR'] += 1; + + +### START DISPLAY + print "

PHP ".PHP_VERSION."

"; + print "

\$_SESSION['AVAR']={$_SESSION['AVAR']}

"; + + print "
Cookies: "; + print_r($_COOKIE); + + var_dump($_SESSION['MONKEY']); + +### RANDOMLY PERFORM Garbage Collection +### In real-production environment, this is done for you +### by php's session extension, which calls adodb_sess_gc() +### automatically for you. See php.ini's +### session.cookie_lifetime and session.gc_probability + + if (rand() % 5 == 0) { + + print "

Garbage Collection

"; + adodb_sess_gc(10); + + if (rand() % 2 == 0) { + print "

Random own session destroy

"; + session_destroy(); + } + } else { + $DB = ADODB_Session::_conn(); + $sessk = $DB->qstr('%AZ'.rand().time()); + $olddate = $DB->DBTimeStamp(time()-30*24*3600); + $rr = $DB->qstr(rand()); + $DB->Execute("insert into {$options['table']} (sesskey,expiry,expireref,sessdata,created,modified) values ($sessk,$olddate, $rr,'',$olddate,$olddate)"); + } diff --git a/app/vendor/adodb/adodb-php/tests/time.php b/app/vendor/adodb/adodb-php/tests/time.php new file mode 100644 index 000000000..8261e1e92 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/time.php @@ -0,0 +1,16 @@ + +" ); +echo( "Converted: $convertedDate" ); //why is string returned as one day (3 not 4) less for this example?? diff --git a/app/vendor/adodb/adodb-php/tests/tmssql.php b/app/vendor/adodb/adodb-php/tests/tmssql.php new file mode 100644 index 000000000..0f81311aa --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/tmssql.php @@ -0,0 +1,79 @@ +mssql"; + $db = mssql_connect('JAGUAR\vsdotnet','adodb','natsoft') or die('No Connection'); + mssql_select_db('northwind',$db); + + $rs = mssql_query('select getdate() as date',$db); + $o = mssql_fetch_row($rs); + print_r($o); + mssql_free_result($rs); + + print "

Delete

"; flush(); + $rs2 = mssql_query('delete from adoxyz',$db); + $p = mssql_num_rows($rs2); + mssql_free_result($rs2); + +} + +function tpear() +{ +include_once('DB.php'); + + print "

PEAR

"; + $username = 'adodb'; + $password = 'natsoft'; + $hostname = 'JAGUAR\vsdotnet'; + $databasename = 'northwind'; + + $dsn = "mssql://$username:$password@$hostname/$databasename"; + $conn = DB::connect($dsn); + print "date=".$conn->GetOne('select getdate()')."
"; + @$conn->query('create table tester (id integer)'); + print "

Delete

"; flush(); + $rs = $conn->query('delete from tester'); + print "date=".$conn->GetOne('select getdate()')."
"; +} + +function tadodb() +{ +include_once('../adodb.inc.php'); + + print "

ADOdb

"; + $conn = NewADOConnection('mssql'); + $conn->Connect('JAGUAR\vsdotnet','adodb','natsoft','northwind'); +// $conn->debug=1; + print "date=".$conn->GetOne('select getdate()')."
"; + $conn->Execute('create table tester (id integer)'); + print "

Delete

"; flush(); + $rs = $conn->Execute('delete from tester'); + print "date=".$conn->GetOne('select getdate()')."
"; +} + + +$ACCEPTIP = '127.0.0.1'; + +$remote = $_SERVER["REMOTE_ADDR"]; + +if (!empty($ACCEPTIP)) + if ($remote != '127.0.0.1' && $remote != $ACCEPTIP) + die("Unauthorised client: '$remote'"); + +?> +mssql +pear +adodb + + + + + + + + + + + + + +id + + +id + + + +
+ + SQL to be executed only on specific platforms + + insert into mytable ( row1, row2 ) values ( 12, 'postgres stuff' ) + + + insert into mytable ( row1, row2 ) values ( 12, 'mysql stuff' ) + + + INSERT into simple_table ( name, description ) values ( '12', 'Microsoft stuff' ) + + +
\ No newline at end of file diff --git a/app/vendor/adodb/adodb-php/tests/xmlschema.xml b/app/vendor/adodb/adodb-php/tests/xmlschema.xml new file mode 100644 index 000000000..ea48ae2bb --- /dev/null +++ b/app/vendor/adodb/adodb-php/tests/xmlschema.xml @@ -0,0 +1,33 @@ + + + + + An integer row that's a primary key and autoincrements + + + + + A 16 character varchar row that can't be null + + + + row1 + row2 + +
+ + SQL to be executed only on specific platforms + + insert into mytable ( row1, row2 ) values ( 12, 'postgres stuff' ) + + + insert into mytable ( row1, row2 ) values ( 12, 'mysql stuff' ) + + + insert into mytable ( row1, row2 ) values ( 12, 'Microsoft stuff' ) + + + + +
+
\ No newline at end of file diff --git a/app/vendor/adodb/adodb-php/toexport.inc.php b/app/vendor/adodb/adodb-php/toexport.inc.php new file mode 100644 index 000000000..94c394b8e --- /dev/null +++ b/app/vendor/adodb/adodb-php/toexport.inc.php @@ -0,0 +1,136 @@ +FieldTypesArray(); + reset($fieldTypes); + $i = 0; + $elements = array(); + while(list(,$o) = each($fieldTypes)) { + + $v = ($o) ? $o->name : 'Field'.($i++); + if ($escquote) $v = str_replace($quote,$escquotequote,$v); + $v = strip_tags(str_replace("\n", $replaceNewLine, str_replace("\r\n",$replaceNewLine,str_replace($sep,$sepreplace,$v)))); + $elements[] = $v; + + } + $s .= implode($sep, $elements).$NEWLINE; + } + $hasNumIndex = isset($rs->fields[0]); + + $line = 0; + $max = $rs->FieldCount(); + + while (!$rs->EOF) { + $elements = array(); + $i = 0; + + if ($hasNumIndex) { + for ($j=0; $j < $max; $j++) { + $v = $rs->fields[$j]; + if (!is_object($v)) $v = trim($v); + else $v = 'Object'; + if ($escquote) $v = str_replace($quote,$escquotequote,$v); + $v = strip_tags(str_replace("\n", $replaceNewLine, str_replace("\r\n",$replaceNewLine,str_replace($sep,$sepreplace,$v)))); + + if (strpos($v,$sep) !== false || strpos($v,$quote) !== false) $elements[] = "$quote$v$quote"; + else $elements[] = $v; + } + } else { // ASSOCIATIVE ARRAY + foreach($rs->fields as $v) { + if ($escquote) $v = str_replace($quote,$escquotequote,trim($v)); + $v = strip_tags(str_replace("\n", $replaceNewLine, str_replace("\r\n",$replaceNewLine,str_replace($sep,$sepreplace,$v)))); + + if (strpos($v,$sep) !== false || strpos($v,$quote) !== false) $elements[] = "$quote$v$quote"; + else $elements[] = $v; + } + } + $s .= implode($sep, $elements).$NEWLINE; + $rs->MoveNext(); + $line += 1; + if ($fp && ($line % $BUFLINES) == 0) { + if ($fp === true) echo $s; + else fwrite($fp,$s); + $s = ''; + } + } + + if ($fp) { + if ($fp === true) echo $s; + else fwrite($fp,$s); + $s = ''; + } + + return $s; +} diff --git a/app/vendor/adodb/adodb-php/tohtml.inc.php b/app/vendor/adodb/adodb-php/tohtml.inc.php new file mode 100644 index 000000000..d39ed2919 --- /dev/null +++ b/app/vendor/adodb/adodb-php/tohtml.inc.php @@ -0,0 +1,201 @@ + +*/ + +// specific code for tohtml +GLOBAL $gSQLMaxRows,$gSQLBlockRows,$ADODB_ROUND; + +$ADODB_ROUND=4; // rounding +$gSQLMaxRows = 1000; // max no of rows to download +$gSQLBlockRows=20; // max no of rows per table block + +// RecordSet to HTML Table +//------------------------------------------------------------ +// Convert a recordset to a html table. Multiple tables are generated +// if the number of rows is > $gSQLBlockRows. This is because +// web browsers normally require the whole table to be downloaded +// before it can be rendered, so we break the output into several +// smaller faster rendering tables. +// +// $rs: the recordset +// $ztabhtml: the table tag attributes (optional) +// $zheaderarray: contains the replacement strings for the headers (optional) +// +// USAGE: +// include('adodb.inc.php'); +// $db = ADONewConnection('mysql'); +// $db->Connect('mysql','userid','password','database'); +// $rs = $db->Execute('select col1,col2,col3 from table'); +// rs2html($rs, 'BORDER=2', array('Title1', 'Title2', 'Title3')); +// $rs->Close(); +// +// RETURNS: number of rows displayed + + +function rs2html(&$rs,$ztabhtml=false,$zheaderarray=false,$htmlspecialchars=true,$echo = true) +{ +$s ='';$rows=0;$docnt = false; +GLOBAL $gSQLMaxRows,$gSQLBlockRows,$ADODB_ROUND; + + if (!$rs) { + printf(ADODB_BAD_RS,'rs2html'); + return false; + } + + if (! $ztabhtml) $ztabhtml = "BORDER='1' WIDTH='98%'"; + //else $docnt = true; + $typearr = array(); + $ncols = $rs->FieldCount(); + $hdr = "\n\n"; + for ($i=0; $i < $ncols; $i++) { + $field = $rs->FetchField($i); + if ($field) { + if ($zheaderarray) $fname = $zheaderarray[$i]; + else $fname = htmlspecialchars($field->name); + $typearr[$i] = $rs->MetaType($field->type,$field->max_length); + //print " $field->name $field->type $typearr[$i] "; + } else { + $fname = 'Field '.($i+1); + $typearr[$i] = 'C'; + } + if (strlen($fname)==0) $fname = ' '; + $hdr .= ""; + } + $hdr .= "\n"; + if ($echo) print $hdr."\n\n"; + else $html = $hdr; + + // smart algorithm - handles ADODB_FETCH_MODE's correctly by probing... + $numoffset = isset($rs->fields[0]) ||isset($rs->fields[1]) || isset($rs->fields[2]); + while (!$rs->EOF) { + + $s .= "\n"; + + for ($i=0; $i < $ncols; $i++) { + if ($i===0) $v=($numoffset) ? $rs->fields[0] : reset($rs->fields); + else $v = ($numoffset) ? $rs->fields[$i] : next($rs->fields); + + $type = $typearr[$i]; + switch($type) { + case 'D': + if (strpos($v,':') !== false); + else { + if (empty($v)) { + $s .= "\n"; + } else { + $s .= " \n"; + } + break; + } + case 'T': + if (empty($v)) $s .= "\n"; + else $s .= " \n"; + break; + + case 'N': + if (abs(abs($v) - round($v,0)) < 0.00000001) + $v = round($v); + else + $v = round($v,$ADODB_ROUND); + case 'I': + $vv = stripslashes((trim($v))); + if (strlen($vv) == 0) $vv .= ' '; + $s .= " \n"; + + break; + /* + case 'B': + if (substr($v,8,2)=="BM" ) $v = substr($v,8); + $mtime = substr(str_replace(' ','_',microtime()),2); + $tmpname = "tmp/".uniqid($mtime).getmypid(); + $fd = @fopen($tmpname,'a'); + @ftruncate($fd,0); + @fwrite($fd,$v); + @fclose($fd); + if (!function_exists ("mime_content_type")) { + function mime_content_type ($file) { + return exec("file -bi ".escapeshellarg($file)); + } + } + $t = mime_content_type($tmpname); + $s .= (substr($t,0,5)=="image") ? " \\n" : " \\n"; + break; + */ + + default: + if ($htmlspecialchars) $v = htmlspecialchars(trim($v)); + $v = trim($v); + if (strlen($v) == 0) $v = ' '; + $s .= " \n"; + + } + } // for + $s .= "\n\n"; + + $rows += 1; + if ($rows >= $gSQLMaxRows) { + $rows = "

Truncated at $gSQLMaxRows

"; + break; + } // switch + + $rs->MoveNext(); + + // additional EOF check to prevent a widow header + if (!$rs->EOF && $rows % $gSQLBlockRows == 0) { + + //if (connection_aborted()) break;// not needed as PHP aborts script, unlike ASP + if ($echo) print $s . "
$fname
  ".$rs->UserDate($v,"D d, M Y") ."   ".$rs->UserTimeStamp($v,"D d, M Y, H:i:s") ."".$vv ."$t$t". str_replace("\n",'
',stripslashes($v)) ."
\n\n"; + else $html .= $s ."\n\n"; + $s = $hdr; + } + } // while + + if ($echo) print $s."\n\n"; + else $html .= $s."\n\n"; + + if ($docnt) if ($echo) print "

".$rows." Rows

"; + + return ($echo) ? $rows : $html; + } + +// pass in 2 dimensional array +function arr2html(&$arr,$ztabhtml='',$zheaderarray='') +{ + if (!$ztabhtml) $ztabhtml = 'BORDER=1'; + + $s = "";//';print_r($arr); + + if ($zheaderarray) { + $s .= ''; + for ($i=0; $i\n"; + } else $s .= " \n"; + $s .= "\n\n"; + } + $s .= '
 
'; + print $s; +} diff --git a/app/vendor/adodb/adodb-php/xmlschema.dtd b/app/vendor/adodb/adodb-php/xmlschema.dtd new file mode 100644 index 000000000..2d0b579d3 --- /dev/null +++ b/app/vendor/adodb/adodb-php/xmlschema.dtd @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +] > \ No newline at end of file diff --git a/app/vendor/adodb/adodb-php/xmlschema03.dtd b/app/vendor/adodb/adodb-php/xmlschema03.dtd new file mode 100644 index 000000000..97850bc7f --- /dev/null +++ b/app/vendor/adodb/adodb-php/xmlschema03.dtd @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]> \ No newline at end of file diff --git a/app/vendor/adodb/adodb-php/xsl/convert-0.1-0.2.xsl b/app/vendor/adodb/adodb-php/xsl/convert-0.1-0.2.xsl new file mode 100644 index 000000000..5b2e3cecd --- /dev/null +++ b/app/vendor/adodb/adodb-php/xsl/convert-0.1-0.2.xsl @@ -0,0 +1,205 @@ + + + + + + + +ADODB XMLSchema +http://adodb-xmlschema.sourceforge.net + + + + 0.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/vendor/adodb/adodb-php/xsl/convert-0.1-0.3.xsl b/app/vendor/adodb/adodb-php/xsl/convert-0.1-0.3.xsl new file mode 100644 index 000000000..3202dce43 --- /dev/null +++ b/app/vendor/adodb/adodb-php/xsl/convert-0.1-0.3.xsl @@ -0,0 +1,221 @@ + + + + + + + +ADODB XMLSchema +http://adodb-xmlschema.sourceforge.net + + + + 0.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/vendor/adodb/adodb-php/xsl/convert-0.2-0.1.xsl b/app/vendor/adodb/adodb-php/xsl/convert-0.2-0.1.xsl new file mode 100644 index 000000000..6398e3e55 --- /dev/null +++ b/app/vendor/adodb/adodb-php/xsl/convert-0.2-0.1.xsl @@ -0,0 +1,207 @@ + + + + + + + +ADODB XMLSchema +http://adodb-xmlschema.sourceforge.net + + + + 0.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/vendor/adodb/adodb-php/xsl/convert-0.2-0.3.xsl b/app/vendor/adodb/adodb-php/xsl/convert-0.2-0.3.xsl new file mode 100644 index 000000000..9e1f2ae34 --- /dev/null +++ b/app/vendor/adodb/adodb-php/xsl/convert-0.2-0.3.xsl @@ -0,0 +1,281 @@ + + + + + + + +ADODB XMLSchema +http://adodb-xmlschema.sourceforge.net + + + + 0.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/vendor/adodb/adodb-php/xsl/remove-0.2.xsl b/app/vendor/adodb/adodb-php/xsl/remove-0.2.xsl new file mode 100644 index 000000000..c82c3ad88 --- /dev/null +++ b/app/vendor/adodb/adodb-php/xsl/remove-0.2.xsl @@ -0,0 +1,54 @@ + + + + + + + +ADODB XMLSchema +http://adodb-xmlschema.sourceforge.net + + + +Uninstallation Schema + + + + 0.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/vendor/adodb/adodb-php/xsl/remove-0.3.xsl b/app/vendor/adodb/adodb-php/xsl/remove-0.3.xsl new file mode 100644 index 000000000..4b1cd02ee --- /dev/null +++ b/app/vendor/adodb/adodb-php/xsl/remove-0.3.xsl @@ -0,0 +1,54 @@ + + + + + + + +ADODB XMLSchema +http://adodb-xmlschema.sourceforge.net + + + +Uninstallation Schema + + + + 0.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/vendor/ajgl/breakpoint-twig-extension/.gitignore b/app/vendor/ajgl/breakpoint-twig-extension/.gitignore new file mode 100644 index 000000000..25be7c4d2 --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +*.cache +composer.lock +phpunit.xml diff --git a/app/vendor/ajgl/breakpoint-twig-extension/.php_cs b/app/vendor/ajgl/breakpoint-twig-extension/.php_cs new file mode 100644 index 000000000..881163ac1 --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/.php_cs @@ -0,0 +1,31 @@ + + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. +EOF; + +Symfony\CS\Fixer\Contrib\HeaderCommentFixer::setHeader($header); + +return Symfony\CS\Config\Config::create() + ->setUsingCache(true) + // use default SYMFONY_LEVEL and extra fixers: + ->fixers(array( + '-psr0', + 'header_comment', + 'newline_after_open_tag', + 'ordered_use', + 'phpdoc_order', + 'strict', + 'strict_param', + )) + ->finder( + Symfony\CS\Finder\DefaultFinder::create() + ->in(__DIR__.'/src') + ->in(__DIR__.'/tests') + ) +; diff --git a/app/vendor/ajgl/breakpoint-twig-extension/.scrutinizer.yml b/app/vendor/ajgl/breakpoint-twig-extension/.scrutinizer.yml new file mode 100644 index 000000000..8b19e9e4c --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/.scrutinizer.yml @@ -0,0 +1,7 @@ +# .scrutinizer.yml +checks: + php: true + +tools: + external_code_coverage: + timeout: 900 diff --git a/app/vendor/ajgl/breakpoint-twig-extension/.travis.yml b/app/vendor/ajgl/breakpoint-twig-extension/.travis.yml new file mode 100644 index 000000000..f443d1321 --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/.travis.yml @@ -0,0 +1,22 @@ +language: php + +matrix: + fast_finish: true + include: + - php: 5.6 + env: COMPOSER_FLAGS="--prefer-lowest" + - php: 5.6 + - php: 7.0 + - php: 7.1 + - php: hhvm + - php: nightly + allow_failures: + - php: hhvm + - php: nightly + +install: + - composer update --prefer-dist --no-interaction $COMPOSER_FLAGS +script: vendor/bin/phpunit --coverage-clover=coverage.clover +after_script: + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover coverage.clover diff --git a/app/vendor/ajgl/breakpoint-twig-extension/CHANGELOG.md b/app/vendor/ajgl/breakpoint-twig-extension/CHANGELOG.md new file mode 100644 index 000000000..58409ccb6 --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/CHANGELOG.md @@ -0,0 +1,35 @@ +# [CHANGELOG](http://keepachangelog.com/) +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased][unreleased] + +## [0.3.1] - 2017-11-20 + +### Changed +- Require PHP 5.6 + +### Fixed +- Update requirements to allow twig 2.x + +## [0.3.0] - 2016-03-31 + +### Added +- Add `$environment` and `$context` variables +- Allow to inspect function arguments + +## [0.2.0] - 2016-03-10 + +### Added +- Add Symfony Bundle + +## 0.1.0 - 2016-03-09 + +### Added +- Add `breakpoint` function + + +[unreleased]: https://github.com/ajgarlag/AjglBreakpointTwigExtension/compare/0.3.1...master +[0.3.1]: https://github.com/ajgarlag/AjglBreakpointTwigExtension/compare/0.3.0...0.3.1 +[0.3.0]: https://github.com/ajgarlag/AjglBreakpointTwigExtension/compare/0.2.0...0.3.0 +[0.2.0]: https://github.com/ajgarlag/AjglBreakpointTwigExtension/compare/0.1.0...0.2.0 diff --git a/app/vendor/ajgl/breakpoint-twig-extension/LICENSE b/app/vendor/ajgl/breakpoint-twig-extension/LICENSE new file mode 100644 index 000000000..9aeafa277 --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2013-2016 Antonio J. García Lagar + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of +the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/app/vendor/ajgl/breakpoint-twig-extension/README.md b/app/vendor/ajgl/breakpoint-twig-extension/README.md new file mode 100644 index 000000000..104d35241 --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/README.md @@ -0,0 +1,113 @@ +AjglBreakpointTwigExtension +=========================== + +The AjglBreakpointTwigExtension component allows you set breakpoints in twig templates. + +[![Build Status](https://travis-ci.org/ajgarlag/AjglBreakpointTwigExtension.png?branch=master)](https://travis-ci.org/ajgarlag/AjglBreakpointTwigExtension) +[![Latest Stable Version](https://poser.pugx.org/ajgl/breakpoint-twig-extension/v/stable.png)](https://packagist.org/packages/ajgl/breakpoint-twig-extension) +[![Latest Unstable Version](https://poser.pugx.org/ajgl/breakpoint-twig-extension/v/unstable.png)](https://packagist.org/packages/ajgl/breakpoint-twig-extension) +[![Total Downloads](https://poser.pugx.org/ajgl/breakpoint-twig-extension/downloads.png)](https://packagist.org/packages/ajgl/breakpoint-twig-extension) +[![Montly Downloads](https://poser.pugx.org/ajgl/breakpoint-twig-extension/d/monthly.png)](https://packagist.org/packages/ajgl/breakpoint-twig-extension) +[![Daily Downloads](https://poser.pugx.org/ajgl/breakpoint-twig-extension/d/daily.png)](https://packagist.org/packages/ajgl/breakpoint-twig-extension) +[![License](https://poser.pugx.org/ajgl/breakpoint-twig-extension/license.png)](https://packagist.org/packages/ajgl/breakpoint-twig-extension) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/ajgarlag/AjglBreakpointTwigExtension/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/ajgarlag/AjglBreakpointTwigExtension/?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/ajgarlag/AjglBreakpointTwigExtension/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/ajgarlag/AjglBreakpointTwigExtension/?branch=master) +[![SensioLabsInsight](https://insight.sensiolabs.com/projects/e0f1276d-6ded-4a20-9b3f-1a7c77a92015/mini.png)](https://insight.sensiolabs.com/projects/e0f1276d-6ded-4a20-9b3f-1a7c77a92015) +[![StyleCI](https://styleci.io/repos/53512207/shield)](https://styleci.io/repos/53512207) + +This component requires the [Xdebug] PHP extension to be installed. + + +Installation +------------ + +To install the latest stable version of this component, open a console and execute the following command: +``` +$ composer require ajgl/breakpoint-twig-extension +``` + + +Usage +----- + +The first step is to register the extension into the twig environment +```php +/* @var $twig Twig_Environment */ +$twig->addExtension(new Ajgl\Twig\Extension\BreakpointExtension()); +``` + +Once registered, you can call the new `breakpoint` function: +```twig + + + + + title + + + {{ breakpoint() }} + + +``` + +Once stopped, your debugger will allow you to inspect the `$environment` and `$context` variables. + +### Function arguments + +Any argument passed to the twig function will be added to the `$arguments` array, so you can inspect it easily. + +```twig + + + + + title + + + {{ breakpoint(app.user, app.session) }} + + +``` + +Symfony Bundle +-------------- + +If you want to use this extension in your Symfony application, you can enable the +Symfony Bundle included in this package: + +```php +// app/AppKernel.php +if (in_array($this->getEnvironment(), array('dev', 'test'), true)) { + $bundles[] = new Ajgl\Twig\Extension\SymfonyBundle\AjglBreakpointTwigExtensionBundle(); +} +``` + +This bundle will register the twig extension automatically. So, once enabled, you +can insert the `breakpoint` twig function in your templates. + + +License +------- + +This component is under the MIT license. See the complete license in the [LICENSE] file. + + +Reporting an issue or a feature request +--------------------------------------- + +Issues and feature requests are tracked in the [Github issue tracker]. + + +Author Information +------------------ + +Developed with ♥ by [Antonio J. García Lagar]. + +If you find this component useful, please add a ★ in the [GitHub repository page] and/or the [Packagist package page]. + +[Xdebug]: https://xdebug.org/ +[LICENSE]: LICENSE +[Github issue tracker]: https://github.com/ajgarlag/AjglBreakpointTwigExtension/issues +[Antonio J. García Lagar]: http://aj.garcialagar.es +[GitHub repository page]: https://github.com/ajgarlag/AjglBreakpointTwigExtension +[Packagist package page]: https://packagist.org/packages/ajgl/breakpoint-twig-extension diff --git a/app/vendor/ajgl/breakpoint-twig-extension/composer.json b/app/vendor/ajgl/breakpoint-twig-extension/composer.json new file mode 100644 index 000000000..a6741b043 --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/composer.json @@ -0,0 +1,40 @@ +{ + "name": "ajgl/breakpoint-twig-extension", + "description": "Twig extension to set breakpoints", + "keywords": ["twig", "xdebug", "breakpoint"], + "homepage": "https://github.com/ajgarlag/AjglBreakpointTwigExtension", + "license": "MIT", + "authors": [ + { + "name": "Antonio J. García Lagar", + "email": "aj@garcialagar.es", + "homepage": "http://aj.garcialagar.es", + "role": "developer" + } + ], + "autoload": { + "psr-4": { "Ajgl\\Twig\\Extension\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "Ajgl\\Twig\\Extension\\Tests\\": "tests/" } + }, + "require": { + "php": ">=5.6", + "twig/twig": "^1.14|^2.0" + }, + "require-dev": { + "symfony/framework-bundle": "^2.7|^3.2", + "symfony/twig-bundle": "^2.7|^3.2", + "phpunit/phpunit": "^5" + }, + "suggest": { + "ext-xdebug": "The Xdebug extension is required for the breakpoint to work", + "symfony/framework-bundle": "The framework bundle to integrate the extension into Symfony", + "symfony/twig-bundle": "The twig bundle to integrate the extension into Symfony" + }, + "extra": { + "branch-alias": { + "dev-master": "0.3.x-dev" + } + } +} diff --git a/app/vendor/ajgl/breakpoint-twig-extension/phpunit.xml.dist b/app/vendor/ajgl/breakpoint-twig-extension/phpunit.xml.dist new file mode 100644 index 000000000..ec8dc6e48 --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + + + ./tests + + + + + + ./src + + + + diff --git a/app/vendor/ajgl/breakpoint-twig-extension/src/BreakpointExtension.php b/app/vendor/ajgl/breakpoint-twig-extension/src/BreakpointExtension.php new file mode 100644 index 000000000..ed9df17df --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/src/BreakpointExtension.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ajgl\Twig\Extension; + +use Twig_Environment; +use Twig_Extension; + +/** + * @author Antonio J. García Lagar + */ +class BreakpointExtension extends Twig_Extension +{ + public function getName() + { + return 'breakpoint'; + } + + public function getFunctions() + { + return array( + new \Twig_SimpleFunction('breakpoint', array($this, 'setBreakpoint'), array('needs_environment' => true, 'needs_context' => true)), + ); + } + + /** + * If XDebug is detected, makes the debugger break. + * + * @param Twig_Environment $environment the environment instance + * @param mixed $context variables from the Twig template + */ + public function setBreakpoint(Twig_Environment $environment, $context) + { + if (function_exists('xdebug_break')) { + $arguments = array_slice(func_get_args(), 2); + xdebug_break(); + } + } +} diff --git a/app/vendor/ajgl/breakpoint-twig-extension/src/SymfonyBundle/AjglBreakpointTwigExtensionBundle.php b/app/vendor/ajgl/breakpoint-twig-extension/src/SymfonyBundle/AjglBreakpointTwigExtensionBundle.php new file mode 100644 index 000000000..2caa7ca86 --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/src/SymfonyBundle/AjglBreakpointTwigExtensionBundle.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ajgl\Twig\Extension\SymfonyBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +/** + * @author Antonio J. García Lagar + */ +class AjglBreakpointTwigExtensionBundle extends Bundle +{ +} diff --git a/app/vendor/ajgl/breakpoint-twig-extension/src/SymfonyBundle/DependencyInjection/AjglBreakpointTwigExtensionExtension.php b/app/vendor/ajgl/breakpoint-twig-extension/src/SymfonyBundle/DependencyInjection/AjglBreakpointTwigExtensionExtension.php new file mode 100644 index 000000000..cf25daac5 --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/src/SymfonyBundle/DependencyInjection/AjglBreakpointTwigExtensionExtension.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ajgl\Twig\Extension\SymfonyBundle\DependencyInjection; + +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; + +/** + * @author Antonio J. García Lagar + */ +class AjglBreakpointTwigExtensionExtension extends Extension +{ + public function load(array $config, ContainerBuilder $container) + { + $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('twig.xml'); + } +} diff --git a/app/vendor/ajgl/breakpoint-twig-extension/src/SymfonyBundle/Resources/config/twig.xml b/app/vendor/ajgl/breakpoint-twig-extension/src/SymfonyBundle/Resources/config/twig.xml new file mode 100644 index 000000000..e7bd8321d --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/src/SymfonyBundle/Resources/config/twig.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/app/vendor/ajgl/breakpoint-twig-extension/tests/BreakpointExtensionTest.php b/app/vendor/ajgl/breakpoint-twig-extension/tests/BreakpointExtensionTest.php new file mode 100644 index 000000000..62bf7a2b5 --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/tests/BreakpointExtensionTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ajgl\Twig\Extension\Tests; + +use Ajgl\Twig\Extension\BreakpointExtension; + +/** + * @author Antonio J. García Lagar + */ +class BreakpointExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var BreakpointExtension + */ + protected $extension; + + protected function setUp() + { + $this->extension = new BreakpointExtension(); + } + + public function testGetName() + { + $this->assertSame('breakpoint', $this->extension->getName()); + } + + public function testGetFunctions() + { + $functions = $this->extension->getFunctions(); + $this->assertCount(1, $functions); + $function = reset($functions); + $this->assertInstanceOf('Twig_SimpleFunction', $function); + $callable = $function->getCallable(); + $this->assertTrue(is_array($callable)); + $this->assertCount(2, $callable); + $this->assertSame($this->extension, $callable[0]); + $this->assertSame('setBreakpoint', $callable[1]); + } +} diff --git a/app/vendor/ajgl/breakpoint-twig-extension/tests/SymfonyBundle/DependencyInjection/AjglBreakpointTwigExtensionExtensionTest.php b/app/vendor/ajgl/breakpoint-twig-extension/tests/SymfonyBundle/DependencyInjection/AjglBreakpointTwigExtensionExtensionTest.php new file mode 100644 index 000000000..3b3aa9fbe --- /dev/null +++ b/app/vendor/ajgl/breakpoint-twig-extension/tests/SymfonyBundle/DependencyInjection/AjglBreakpointTwigExtensionExtensionTest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ajgl\Twig\Extension\Tests\SymfonyBundle\DependencyInjection; + +use Ajgl\Twig\Extension\SymfonyBundle\DependencyInjection\AjglBreakpointTwigExtensionExtension; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Antonio J. García Lagar + */ +class AjglBreakpointTwigExtensionExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ContainerBuilder + */ + protected $container; + + /** + * @var AjglBreakpointTwigExtensionExtension + */ + protected $extension; + + protected function setUp() + { + $this->container = new ContainerBuilder(); + $this->extension = new AjglBreakpointTwigExtensionExtension(); + } + + public function testTwigExtensionsDefinition() + { + $this->extension->load(array(), $this->container); + $this->assertTrue($this->container->hasDefinition('ajgl_twig_extension.breakpoint')); + $definition = $this->container->getDefinition('ajgl_twig_extension.breakpoint'); + $this->assertSame( + 'Ajgl\Twig\Extension\BreakpointExtension', + $definition->getClass() + ); + $this->assertNotNull($definition->getTag('twig.extension')); + } +} diff --git a/app/vendor/aptoma/twig-markdown/.gitignore b/app/vendor/aptoma/twig-markdown/.gitignore new file mode 100644 index 000000000..912051563 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/.gitignore @@ -0,0 +1,3 @@ +vendor +composer.phar +phpunit.xml diff --git a/app/vendor/aptoma/twig-markdown/.travis.yml b/app/vendor/aptoma/twig-markdown/.travis.yml new file mode 100644 index 000000000..62d7420dc --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/.travis.yml @@ -0,0 +1,27 @@ +language: php +sudo: false + +php: + - 5.4 + - 5.5 + - 5.6 + - 7 + +env: + global: + # travis encrypt CODECLIMATE_REPO_TOKEN= + - secure: "n4I9GwRCxi9UGjhDGe79SV6Nr/fRKpA8N+qqqbkveJv01Z4IRcAsRBRxKrSAX8zJUoFhlJK73VeRnBfdSBddsL3RqgDMbHilZFy6yC7Dj/vVyNe90RPWxc5J7w3sidT2g1OsgWueiUis8y3dGOQd2sewfcb2pZa29/eQusBV5i0=" + +install: + - composer install + +before_script: + - mkdir -p build/logs + +script: + - vendor/bin/phpunit --coverage-clover build/logs/clover.xml + +after_script: + - php vendor/bin/coveralls -v + - ./vendor/bin/test-reporter --stdout > codeclimate.json + - "curl -X POST -d @codeclimate.json -H 'Content-Type: application/json' -H 'User-Agent: Code Climate (PHP Test Reporter v1.0.1-dev)' https://codeclimate.com/test_reports" diff --git a/app/vendor/aptoma/twig-markdown/CHANGELOG.md b/app/vendor/aptoma/twig-markdown/CHANGELOG.md new file mode 100644 index 000000000..09db06d37 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/CHANGELOG.md @@ -0,0 +1,23 @@ +CHANGELOG +========= + +2.0.0 +----- + +- **BC**: Require Twig v1.12, in order to replace deprecated Twig_Filter_Method with Twig_SimpleFilter +- **Added**: Add support for ParsedownEnging + +1.2.0 +----- + +- **Added**: Add support for GitHub's markdown engine + +1.1.0 +----- + +- **Added**: Add support for PHP League CommonMark engine + +1.0.0 +----- + +- **BC**: Remove deprecated dflydev-markdown parser diff --git a/app/vendor/aptoma/twig-markdown/README.md b/app/vendor/aptoma/twig-markdown/README.md new file mode 100644 index 000000000..b6807ef1a --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/README.md @@ -0,0 +1,171 @@ +Twig Markdown Extension +======================= + +[![Build Status](https://secure.travis-ci.org/aptoma/twig-markdown.png?branch=master)](http://travis-ci.org/aptoma/twig-markdown) +[![Coverage Status](https://img.shields.io/coveralls/aptoma/twig-markdown.svg)](https://coveralls.io/r/aptoma/twig-markdown) + +Twig Markdown extension provides a new filter and a tag to allow parsing of +content as Markdown in [Twig][1] templates. + +This extension could be integrated with several Markdown parser as it provides an interface, which allows you to customize your Markdown parser. + +### Supported parsers + + * [michelf/php-markdown](https://github.com/michelf/php-markdown) (+ MarkdownExtra) + * [league/commonmark](http://commonmark.thephpleague.com/) + * [KnpLabs/php-github-api](https://github.com/KnpLabs/php-github-api) + * [erusev/parsedown](https://github.com/erusev/parsedown) + +## Features + + * Filter support `{{ "# Heading Level 1"|markdown }}` + * Tag support `{% markdown %}{% endmarkdown %}` + +When used as a tag, the indentation level of the first line sets the default indentation level for the rest of the tag content. +From this indentation level, all same indentation or outdented levels text will be transformed as regular text. + +This feature allows you to write your Markdown content at any indentation level without caring of Markdown internal transformation: + +```php +
+

{{ title }}

+ + {% markdown %} + This is a list that is indented to match the context around the markdown tag: + + * List item 1 + * List item 2 + * Sub List Item + * Sub Sub List Item + + The following block will be transformed as code, as it is indented more than the + surrounding content: + + $code = "good"; + + {% endmarkdown %} + +
+``` + +## Installation + +Run the composer command to install the latest stable version: + +```bash +composer require aptoma/twig-markdown +``` + +Or update your `composer.json`: + +```json +{ + "require": { + "aptoma/twig-markdown": "~1.1" + } +} +``` + +You can choose to provide your own Markdown engine, although we recommend +using [michelf/php-markdown](https://github.com/michelf/php-markdown): + +```bash +composer require michelf/php-markdown ~1.3 +``` + +```json +{ + "require": { + "michelf/php-markdown": "~1.3" + } +} +``` + +You may also use the [PHP League CommonMark engine](http://commonmark.thephpleague.com/): + +```bash +composer require league/commonmark ~0.5 +``` + +```json +{ + "require": { + "league/commonmark": "~0.5" + } +} +``` + +## Usage + +### Twig Extension + +The Twig extension provides the `markdown` tag and filter support. + +Assuming that you are using [composer](http://getcomposer.org) autoloading, +add the extension to the Twig environment: + +```php + +use Aptoma\Twig\Extension\MarkdownExtension; +use Aptoma\Twig\Extension\MarkdownEngine; + +$engine = new MarkdownEngine\MichelfMarkdownEngine(); + +$twig->addExtension(new MarkdownExtension($engine)); +``` + +### Twig Token Parser + +The Twig token parser provides the `markdown` tag only! + +```php +use Aptoma\Twig\Extension\MarkdownEngine; +use Aptoma\Twig\TokenParser\MarkdownTokenParser; + +$engine = new MarkdownEngine\MichelfMarkdownEngine(); + +$twig->addTokenParser(new MarkdownTokenParser($engine)); +``` + +### GitHub Markdown Engine + +`MarkdownEngine\GitHubMarkdownEngine` provides an interface to GitHub's markdown engine using their public API via [`KnpLabs\php-github-api`][2]. To reduce API calls, rendered documents are hashed and cached in the filesystem. You can pass a GitHub repository and the path to be used for caching to the constructor: + +```php +use Aptoma\Twig\Extension\MarkdownEngine; + +$engine = new MarkdownEngine\GitHubMarkdownEngine( + 'aptoma/twig-markdown', // The GitHub repository to use as a context + true, // Whether to use GitHub's Flavored Markdown (GFM) + '/tmp/markdown-cache', // Path on filesystem to store rendered documents +); +``` + +In order to authenticate the API client (for instance), it's possible to pass an own instance of `\GitHub\Client` instead of letting the engine create one itself: + +```php +$client = new \GitHub\Client; +$client->authenticate('GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET', \Github\Client::AUTH_URL_CLIENT_ID); + +$engine = new MarkdownEngine\GitHubMarkdownEngine('aptoma/twig-markdown', true, '/tmp/markdown-cache', $client); +``` + +### Using a different Markdown parser engine + +If you want to use a different Markdown parser, you need to create an adapter +that implements `Aptoma\Twig\Extension\MarkdownEngineInterface.php`. Have +a look at `Aptoma\Twig\Extension\MarkdownEngine\MichelfMarkdownEngine` for an +example. + +## Tests + +The test suite uses PHPUnit: + + $ phpunit + +## License + +Twig Markdown Extension is licensed under the MIT license. + +[1]: http://twig.sensiolabs.org +[2]: https://github.com/knplabs/php-github-api diff --git a/app/vendor/aptoma/twig-markdown/composer.json b/app/vendor/aptoma/twig-markdown/composer.json new file mode 100644 index 000000000..491c1f063 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/composer.json @@ -0,0 +1,35 @@ +{ + "name": "aptoma/twig-markdown", + "description": "Twig extension to work with Markdown content", + "keywords": ["twig", "markdown"], + "license": "MIT", + "authors": [ + { + "name": "Gunnar Lium", + "email": "gunnar@aptoma.com" + }, + { + "name": "Joris Berthelot", + "email": "joris@berthelot.tel" + } + ], + "require": { + "twig/twig": "~1.12" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "satooshi/php-coveralls": "~0.6", + "codeclimate/php-test-reporter": "dev-master", + "michelf/php-markdown": "~1", + "league/commonmark": "~0.5", + "knplabs/github-api": "~1.2", + "erusev/parsedown": "^1.6" + }, + "suggest": { + "michelf/php-markdown": "Original Markdown engine with MarkdownExtra.", + "knplabs/github-api": "Needed for using GitHub's Markdown engine provided through their API." + }, + "autoload": { + "psr-0": { "Aptoma": "src/" } + } +} diff --git a/app/vendor/aptoma/twig-markdown/composer.lock b/app/vendor/aptoma/twig-markdown/composer.lock new file mode 100644 index 000000000..dac9b0d0a --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/composer.lock @@ -0,0 +1,1772 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "cc0f13dd7a782442ae3b39805823a56b", + "packages": [ + { + "name": "twig/twig", + "version": "v1.22.3", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "ebfc36b7e77b0c1175afe30459cf943010245540" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/ebfc36b7e77b0c1175afe30459cf943010245540", + "reference": "ebfc36b7e77b0c1175afe30459cf943010245540", + "shasum": "" + }, + "require": { + "php": ">=5.2.7" + }, + "require-dev": { + "symfony/debug": "~2.7", + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.22-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + }, + { + "name": "Twig Team", + "homepage": "http://twig.sensiolabs.org/contributors", + "role": "Contributors" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "http://twig.sensiolabs.org", + "keywords": [ + "templating" + ], + "time": "2015-10-13 07:07:02" + } + ], + "packages-dev": [ + { + "name": "codeclimate/php-test-reporter", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/codeclimate/php-test-reporter.git", + "reference": "892163d67e3bd9c190d43655acb69657da765c7b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codeclimate/php-test-reporter/zipball/892163d67e3bd9c190d43655acb69657da765c7b", + "reference": "892163d67e3bd9c190d43655acb69657da765c7b", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3", + "satooshi/php-coveralls": "0.6.*", + "symfony/console": ">=2.0" + }, + "require-dev": { + "ext-xdebug": "*", + "phpunit/phpunit": "3.7.*@stable" + }, + "bin": [ + "composer/bin/test-reporter" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.2.x-dev" + } + }, + "autoload": { + "psr-0": { + "CodeClimate\\Component": "src/", + "CodeClimate\\Bundle": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Code Climate", + "email": "hello@codeclimate.com", + "homepage": "https://codeclimate.com" + } + ], + "description": "PHP client for reporting test coverage to Code Climate", + "homepage": "https://github.com/codeclimate/php-test-reporter", + "keywords": [ + "codeclimate", + "coverage" + ], + "time": "2015-10-15 14:41:10" + }, + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14 21:17:01" + }, + { + "name": "erusev/parsedown", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "3ebbd730b5c2cf5ce78bc1bf64071407fc6674b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/3ebbd730b5c2cf5ce78bc1bf64071407fc6674b7", + "reference": "3ebbd730b5c2cf5ce78bc1bf64071407fc6674b7", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "time": "2015-10-04 16:44:32" + }, + { + "name": "guzzle/guzzle", + "version": "v3.9.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle3.git", + "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9", + "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.3", + "symfony/event-dispatcher": "~2.1" + }, + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", + "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", + "guzzle/parser": "self.version", + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" + }, + "require-dev": { + "doctrine/cache": "~1.3", + "monolog/monolog": "~1.0", + "phpunit/phpunit": "3.7.*", + "psr/log": "~1.0", + "symfony/class-loader": "~2.1", + "zendframework/zend-cache": "2.*,<2.3", + "zendframework/zend-log": "2.*,<2.3" + }, + "suggest": { + "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.9-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" + } + ], + "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2015-03-18 18:23:50" + }, + { + "name": "knplabs/github-api", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/php-github-api.git", + "reference": "832b7be695ed2733741cd5c79166b4a88fb50786" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/832b7be695ed2733741cd5c79166b4a88fb50786", + "reference": "832b7be695ed2733741cd5c79166b4a88fb50786", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "guzzle/guzzle": "~3.7", + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "knplabs/gaufrette": "Needed for optional Gaufrette cache" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Github\\": "lib/Github/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Thibault Duplessis", + "email": "thibault.duplessis@gmail.com", + "homepage": "http://ornicar.github.com" + }, + { + "name": "KnpLabs Team", + "homepage": "http://knplabs.com" + } + ], + "description": "GitHub API v3 client", + "homepage": "https://github.com/KnpLabs/php-github-api", + "keywords": [ + "api", + "gh", + "gist", + "github" + ], + "time": "2015-10-11 02:38:28" + }, + { + "name": "league/commonmark", + "version": "0.11.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "d22d6a6a4b049faccc2f8e491cce6076eeb165c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d22d6a6a4b049faccc2f8e491cce6076eeb165c7", + "reference": "d22d6a6a4b049faccc2f8e491cce6076eeb165c7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.4.8" + }, + "replace": { + "colinodell/commonmark-php": "*" + }, + "require-dev": { + "erusev/parsedown": "~1.0", + "jgm/commonmark": "0.22", + "jgm/smartpunct": "0.22", + "michelf/php-markdown": "~1.4", + "mikehaertl/php-shellcommand": "~1.1.0", + "phpunit/phpunit": "~4.3", + "phpunit/phpunit-mock-objects": "2.3.0", + "symfony/finder": "~2.3" + }, + "bin": [ + "bin/commonmark" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "http://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Markdown parser for PHP based on the CommonMark spec", + "homepage": "https://github.com/thephpleague/commonmark", + "keywords": [ + "commonmark", + "markdown", + "parser" + ], + "time": "2015-09-25 12:40:32" + }, + { + "name": "michelf/php-markdown", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/michelf/php-markdown.git", + "reference": "e1aabe18173231ebcefc90e615565742fc1c7fd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/michelf/php-markdown/zipball/e1aabe18173231ebcefc90e615565742fc1c7fd9", + "reference": "e1aabe18173231ebcefc90e615565742fc1c7fd9", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-lib": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Michelf": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "John Gruber", + "homepage": "http://daringfireball.net/" + }, + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "https://michelf.ca/", + "role": "Developer" + } + ], + "description": "PHP Markdown", + "homepage": "https://michelf.ca/projects/php-markdown/", + "keywords": [ + "markdown" + ], + "time": "2015-03-01 12:03:08" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2015-02-03 12:10:50" + }, + { + "name": "phpspec/prophecy", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4745ded9307786b730d7a60df5cb5a6c43cf95f7", + "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "phpdocumentor/reflection-docblock": "~2.0", + "sebastian/comparator": "~1.1" + }, + "require-dev": { + "phpspec/phpspec": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2015-08-13 10:07:40" + }, + { + "name": "phpunit/php-code-coverage", + "version": "2.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "~1.3", + "sebastian/environment": "^1.3.2", + "sebastian/version": "~1.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~4" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2015-10-06 15:47:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2015-06-21 13:08:43" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21 13:50:34" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2015-06-21 08:01:12" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2015-09-15 10:49:45" + }, + { + "name": "phpunit/phpunit", + "version": "4.8.14", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b4900675926860bef091644849305399b986efa2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b4900675926860bef091644849305399b986efa2", + "reference": "b4900675926860bef091644849305399b986efa2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "~2.1", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": ">=1.0.6", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.1", + "sebastian/diff": "~1.2", + "sebastian/environment": "~1.3", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/version": "~1.0", + "symfony/yaml": "~2.1|~3.0" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.8.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2015-10-17 15:03:30" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "2.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": ">=5.3.3", + "phpunit/php-text-template": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2015-10-02 06:51:40" + }, + { + "name": "psr/log", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Psr\\Log\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2012-12-21 11:40:51" + }, + { + "name": "satooshi/php-coveralls", + "version": "v0.6.1", + "source": { + "type": "git", + "url": "https://github.com/satooshi/php-coveralls.git", + "reference": "dd0df95bd37a7cf5c5c50304dfe260ffe4b50760" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/satooshi/php-coveralls/zipball/dd0df95bd37a7cf5c5c50304dfe260ffe4b50760", + "reference": "dd0df95bd37a7cf5c5c50304dfe260ffe4b50760", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-simplexml": "*", + "guzzle/guzzle": ">=3.0", + "php": ">=5.3", + "psr/log": "1.0.0", + "symfony/config": ">=2.0", + "symfony/console": ">=2.0", + "symfony/stopwatch": ">=2.2", + "symfony/yaml": ">=2.0" + }, + "require-dev": { + "apigen/apigen": "2.8.*@stable", + "pdepend/pdepend": "dev-master", + "phpmd/phpmd": "dev-master", + "phpunit/php-invoker": ">=1.1.0,<1.2.0", + "phpunit/phpunit": "3.7.*@stable", + "sebastian/finder-facade": "dev-master", + "sebastian/phpcpd": "1.4.*@stable", + "squizlabs/php_codesniffer": "1.4.*@stable", + "theseer/fdomdocument": "dev-master" + }, + "bin": [ + "composer/bin/coveralls" + ], + "type": "library", + "autoload": { + "psr-0": { + "Contrib\\Component": "src/", + "Contrib\\Bundle": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kitamura Satoshi", + "email": "with.no.parachute@gmail.com", + "homepage": "https://www.facebook.com/satooshi.jp" + } + ], + "description": "PHP client library for Coveralls API", + "homepage": "https://github.com/satooshi/php-coveralls", + "keywords": [ + "ci", + "coverage", + "github", + "test" + ], + "time": "2013-05-04 08:07:33" + }, + { + "name": "sebastian/comparator", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2015-07-26 15:48:44" + }, + { + "name": "sebastian/diff", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/863df9687835c62aa423a22412d26fa2ebde3fd3", + "reference": "863df9687835c62aa423a22412d26fa2ebde3fd3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "http://www.github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-02-22 15:13:53" + }, + { + "name": "sebastian/environment", + "version": "1.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6324c907ce7a52478eeeaede764f48733ef5ae44", + "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2015-08-03 06:14:51" + }, + { + "name": "sebastian/exporter", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "7ae5513327cb536431847bcc0c10edba2701064e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e", + "reference": "7ae5513327cb536431847bcc0c10edba2701064e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2015-06-21 07:55:53" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12 03:26:01" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "994d4a811bafe801fb06dccbee797863ba2792ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/994d4a811bafe801fb06dccbee797863ba2792ba", + "reference": "994d4a811bafe801fb06dccbee797863ba2792ba", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-06-21 08:04:50" + }, + { + "name": "sebastian/version", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2015-06-21 13:59:46" + }, + { + "name": "symfony/config", + "version": "v2.7.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "9698fdf0a750d6887d5e7729d5cf099765b20e61" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/9698fdf0a750d6887d5e7729d5cf099765b20e61", + "reference": "9698fdf0a750d6887d5e7729d5cf099765b20e61", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/filesystem": "~2.3" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Config Component", + "homepage": "https://symfony.com", + "time": "2015-09-21 15:02:29" + }, + { + "name": "symfony/console", + "version": "v2.7.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "06cb17c013a82f94a3d840682b49425cd00a2161" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/06cb17c013a82f94a3d840682b49425cd00a2161", + "reference": "06cb17c013a82f94a3d840682b49425cd00a2161", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.1", + "symfony/phpunit-bridge": "~2.7", + "symfony/process": "~2.1" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2015-09-25 08:32:23" + }, + { + "name": "symfony/event-dispatcher", + "version": "v2.7.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae4dcc2a8d3de98bd794167a3ccda1311597c5d9", + "reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.0,>=2.0.5", + "symfony/dependency-injection": "~2.6", + "symfony/expression-language": "~2.6", + "symfony/phpunit-bridge": "~2.7", + "symfony/stopwatch": "~2.3" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2015-09-22 13:49:29" + }, + { + "name": "symfony/filesystem", + "version": "v2.7.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "a17f8a17c20e8614c15b8e116e2f4bcde102cfab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/a17f8a17c20e8614c15b8e116e2f4bcde102cfab", + "reference": "a17f8a17c20e8614c15b8e116e2f4bcde102cfab", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2015-09-09 17:42:36" + }, + { + "name": "symfony/stopwatch", + "version": "v2.7.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "08dd97b3f22ab9ee658cd16e6758f8c3c404336e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/08dd97b3f22ab9ee658cd16e6758f8c3c404336e", + "reference": "08dd97b3f22ab9ee658cd16e6758f8c3c404336e", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Stopwatch Component", + "homepage": "https://symfony.com", + "time": "2015-09-22 13:49:29" + }, + { + "name": "symfony/yaml", + "version": "v2.7.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/31cb2ad0155c95b88ee55fe12bc7ff92232c1770", + "reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2015-09-14 14:14:09" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "codeclimate/php-test-reporter": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/app/vendor/aptoma/twig-markdown/phpunit.xml.dist b/app/vendor/aptoma/twig-markdown/phpunit.xml.dist new file mode 100644 index 000000000..f67a57966 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + diff --git a/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/GitHubMarkdownEngine.php b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/GitHubMarkdownEngine.php new file mode 100644 index 000000000..10905c259 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/GitHubMarkdownEngine.php @@ -0,0 +1,76 @@ + + */ +class GitHubMarkdownEngine implements MarkdownEngineInterface +{ + /** + * Constructor + * + * @param string $contextRepo The repository context. Pass a GitHub repo + * such as 'aptoma/twig-markdown' to render e.g. issues #23 in the + * context of the repo. + * @param bool $gfm Whether to use GitHub's Flavored Markdown or the + * standard markdown. Default is true. + * @param string $cacheDir Location on disk where rendered documents should + * be stored. + * @param \Github\Client $client Client object to use. A new Github\Client() + * object is constructed automatically if $client is null. + */ + public function __construct($contextRepo = null, $gfm = true, $cacheDir = '/tmp/github-markdown-cache', \GitHub\Client $client=null) + { + $this->repo = $contextRepo; + $this->mode = $gfm ? 'gfm' : 'markdown'; + $this->cacheDir = rtrim($cacheDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + if (!is_dir($this->cacheDir)) { + @mkdir($this->cacheDir, 0777, true); + } + + if ($client === null) { + $client = new \Github\Client(); + } + $this->api = $client->api('markdown'); + } + + /** + * {@inheritdoc} + */ + public function transform($content) + { + $cacheFile = $this->getCachePath($content); + if (file_exists($cacheFile)) { + return file_get_contents($cacheFile);; + } + + $response = $this->api->render($content, $this->mode, $this->repo); + file_put_contents($cacheFile, $response); + return $response; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'KnpLabs\php-github-api'; + } + + private function getCachePath($content) + { + return $this->cacheDir . md5($content) . '_' . $this->mode. '_' . str_replace('/', '.', $this->repo); + } + + private $api; + private $cacheDir; + private $repo; + private $mode; +} diff --git a/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/MichelfMarkdownEngine.php b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/MichelfMarkdownEngine.php new file mode 100644 index 000000000..92899eaef --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/MichelfMarkdownEngine.php @@ -0,0 +1,32 @@ + + */ +class MichelfMarkdownEngine implements MarkdownEngineInterface +{ + /** + * {@inheritdoc} + */ + public function transform($content) + { + return MarkdownExtra::defaultTransform($content); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'Michelf\Markdown'; + } +} diff --git a/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/PHPLeagueCommonMarkEngine.php b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/PHPLeagueCommonMarkEngine.php new file mode 100644 index 000000000..e10a7d220 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/PHPLeagueCommonMarkEngine.php @@ -0,0 +1,49 @@ + + */ +class PHPLeagueCommonMarkEngine implements MarkdownEngineInterface +{ + /** + * @var \League\CommonMark\CommonMarkConverter + */ + private $converter; + + /** + * Constructor + * + * Accepts CommonMarkConverter or creates one automatically + * + * @param \League\CommonMark\CommonMarkConverter $converter + */ + public function __construct(CommonMarkConverter $converter = null) + { + $this->converter = $converter ?: new CommonMarkConverter(); + } + + /** + * {@inheritdoc} + */ + public function transform($content) + { + return $this->converter->convertToHtml($content); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'League\CommonMark'; + } +} diff --git a/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/ParsedownEngine.php b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/ParsedownEngine.php new file mode 100644 index 000000000..62ee679cf --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngine/ParsedownEngine.php @@ -0,0 +1,45 @@ + + */ +class ParsedownEngine implements MarkdownEngineInterface +{ + /** + * @var Parsedown + */ + protected $engine; + + /** + * @param string|null $instanceName + */ + public function __construct($instanceName = null) + { + $this->engine = Parsedown::instance($instanceName); + } + + /** + * {@inheritdoc} + */ + public function transform($content) + { + return $this->engine->parse($content); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'erusev/parsedown'; + } +} diff --git a/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngineInterface.php b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngineInterface.php new file mode 100644 index 000000000..cfc34b366 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownEngineInterface.php @@ -0,0 +1,28 @@ + + */ +interface MarkdownEngineInterface +{ + /** + * Transforms the given markdown data in HTML + * + * @param $content Markdown data + * @return string + */ + public function transform($content); + + /** + * Return Markdown engine vendor ID + * + * @return string + */ + public function getName(); +} diff --git a/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownExtension.php b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownExtension.php new file mode 100644 index 000000000..522d49c15 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Extension/MarkdownExtension.php @@ -0,0 +1,69 @@ + + * @author Joris Berthelot + */ +class MarkdownExtension extends \Twig_Extension +{ + + /** + * @var MarkdownEngineInterface $markdownEngine + */ + private $markdownEngine; + + /** + * @param MarkdownEngineInterface $markdownEngine The Markdown parser engine + */ + public function __construct(MarkdownEngineInterface $markdownEngine) + { + $this->markdownEngine = $markdownEngine; + } + + /** + * {@inheritdoc} + */ + public function getFilters() + { + return array( + new \Twig_SimpleFilter( + 'markdown', + array($this, 'parseMarkdown'), + array('is_safe' => array('html')) + ) + ); + } + + /** + * Transform Markdown content to HTML + * + * @param $content The Markdown content to be transformed + * @return string The result of the Markdown engine transformation + */ + public function parseMarkdown($content) + { + return $this->markdownEngine->transform($content); + } + + /** + * {@inheritdoc} + */ + public function getTokenParsers() + { + return array(new MarkdownTokenParser($this->markdownEngine)); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'markdown'; + } +} diff --git a/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Node/MarkdownNode.php b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Node/MarkdownNode.php new file mode 100644 index 000000000..9e38a35b5 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/Node/MarkdownNode.php @@ -0,0 +1,38 @@ + + * @author Joris Berthelot + */ +class MarkdownNode extends \Twig_Node +{ + public function __construct(\Twig_NodeInterface $body, $lineno, $tag = 'markdown') + { + parent::__construct(array('body' => $body), array(), $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param \Twig_Compiler A Twig_Compiler instance + */ + public function compile(\Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('ob_start();' . PHP_EOL) + ->subcompile($this->getNode('body')) + ->write('$content = ob_get_clean();' . PHP_EOL) + ->write('preg_match("/^\s*/", $content, $matches);' . PHP_EOL) + ->write('$lines = explode("\n", $content);' . PHP_EOL) + ->write('$content = preg_replace(\'/^\' . $matches[0]. \'/\', "", $lines);' . PHP_EOL) + ->write('$content = join("\n", $content);' . PHP_EOL) + ->write('echo $this->env->getTokenParsers()->getTokenParser(\'markdown\')->getEngine()->transform($content);' . PHP_EOL); + } +} diff --git a/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/TokenParser/MarkdownTokenParser.php b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/TokenParser/MarkdownTokenParser.php new file mode 100644 index 000000000..9f587f91f --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/src/Aptoma/Twig/TokenParser/MarkdownTokenParser.php @@ -0,0 +1,76 @@ + + * @author Joris Berthelot + */ +class MarkdownTokenParser extends \Twig_TokenParser +{ + /** + * @var The Markdown engine + */ + protected $markdownEngine; + + /** + * @param MarkdownEngineInterface $markdownEngine The Markdown parser engine + */ + public function __construct(MarkdownEngineInterface $markdownEngine) + { + $this->markdownEngine = $markdownEngine; + } + + /** + * Markdown parser engine getter + * + * @return MarkdownEngineInterface + */ + public function getEngine() + { + return $this->markdownEngine; + } + + /** + * Parses a token and returns a node. + * + * @param \Twig_Token $token A \Twig_Token instance + * + * @throws \Twig_Error_Syntax + * @return \Twig_NodeInterface A \Twig_NodeInterface instance + */ + public function parse(\Twig_Token $token) + { + $lineno = $token->getLine(); + + $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); + $body = $this->parser->subparse(array($this, 'decideMarkdownEnd'), true); + $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); + + return new MarkdownNode($body, $lineno, $this->getTag()); + } + + /** + * Decide if current token marks end of Markdown block. + * + * @param \Twig_Token $token + * @return bool + */ + public function decideMarkdownEnd(\Twig_Token $token) + { + return $token->test('endmarkdown'); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'markdown'; + } +} diff --git a/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownEngine/GithubMarkdownEngineTest.php b/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownEngine/GithubMarkdownEngineTest.php new file mode 100644 index 000000000..054205b78 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownEngine/GithubMarkdownEngineTest.php @@ -0,0 +1,31 @@ + + */ +class GitHubMarkdownEngineTest extends MarkdownExtensionTest +{ + public function getParseMarkdownTests() + { + return array( + array('{{ "# Main Title"|markdown }}', '

Main Title

'), + array('{{ content|markdown }}', '

Main Title

', array('content' => '# Main Title')), + // Check if GFM is working + array('{{ "@aptoma"|markdown }}', + '

@aptoma

'), + ); + } + + protected function getEngine() + { + return new GitHubMarkdownEngine(); + } +} diff --git a/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownEngine/PHPLeagueCommonMarkEngineTest.php b/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownEngine/PHPLeagueCommonMarkEngineTest.php new file mode 100644 index 000000000..806ca0266 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownEngine/PHPLeagueCommonMarkEngineTest.php @@ -0,0 +1,23 @@ + + */ +class PHPLeagueCommonMarkEngineTest extends MarkdownExtensionTest +{ + protected function getEngine() + { + return new PHPLeagueCommonMarkEngine(); + } +} diff --git a/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownEngine/ParsedownEngineTest.php b/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownEngine/ParsedownEngineTest.php new file mode 100644 index 000000000..5337ca01f --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownEngine/ParsedownEngineTest.php @@ -0,0 +1,32 @@ + + */ +class ParsedownEngineTest extends MarkdownExtensionTest +{ + public function getParseMarkdownTests() + { + return array( + array('{{ "# Main Title"|markdown }}', '

Main Title

'), + array('{{ content|markdown }}', '

Main Title

', array('content' => '# Main Title')), + array('{% markdown %}{{ content }}{% endmarkdown %}', '

Main Title

', array('content' => '# Main Title')) + ); + } + + protected function getEngine() + { + return new ParsedownEngine(); + } +} diff --git a/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownExtensionTest.php b/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownExtensionTest.php new file mode 100644 index 000000000..b21dad104 --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/Extension/MarkdownExtensionTest.php @@ -0,0 +1,41 @@ + + */ +class MarkdownExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getParseMarkdownTests + */ + public function testParseMarkdown($template, $expected, $context = array()) + { + $this->assertEquals($expected, $this->getTemplate($template)->render($context)); + } + + public function getParseMarkdownTests() + { + return array( + array('{{ "# Main Title"|markdown }}', '

Main Title

' . PHP_EOL), + array('{{ content|markdown }}', '

Main Title

' . PHP_EOL, array('content' => '# Main Title')) + ); + } + + protected function getEngine() + { + return new MichelfMarkdownEngine(); + } + + protected function getTemplate($template) + { + $loader = new \Twig_Loader_Array(array('index' => $template)); + $twig = new \Twig_Environment($loader, array('debug' => true, 'cache' => false)); + $twig->addExtension(new MarkdownExtension($this->getEngine())); + + return $twig->loadTemplate('index'); + } +} diff --git a/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/TokenParser/MarkdownTokenParserTest.php b/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/TokenParser/MarkdownTokenParserTest.php new file mode 100644 index 000000000..34c7a884f --- /dev/null +++ b/app/vendor/aptoma/twig-markdown/tests/Aptoma/Twig/TokenParser/MarkdownTokenParserTest.php @@ -0,0 +1,109 @@ + + */ +class MarkdownTokenParserTest extends \Twig_Test_NodeTestCase +{ + public function testConstructor() + { + $body = new \Twig_Node(array(new \Twig_Node_Text("#Title\n\nparagraph\n", 1))); + $node = new MarkdownNode($body, 1); + + $this->assertEquals($body, $node->getNode('body')); + } + + /** + * Test that the generated code actually do what we expect + * + * The contents of this test is the same that we write in the compile method. + * This requires manual synchronization, which we should probably not rely on. + */ + public function testMarkdownPrepareBehavior() + { + $body = " #Title\n\n paragraph\n\n code"; + $bodyPrepared = "#Title\n\nparagraph\n\n code"; + + ob_start(); + echo $body; + $content = ob_get_clean(); + preg_match("/^\s*/", $content, $matches); + $lines = explode("\n", $content); + $content = preg_replace('/^' . $matches[0]. '/', "", $lines); + $content = join("\n", $content); + + // Assert prepared content looks right + $this->assertEquals($bodyPrepared, $content); + + // Assert Markdown output + $expectedOutput = "

Title

\n\n

paragraph

\n\n
code\n
\n"; + $this->assertEquals($expectedOutput, $this->getEngine()->transform($content)); + } + + /** + * Test that the generated code looks as expected + * + * @dataProvider getTests + */ + public function testCompile($node, $source, $environment = null) + { + parent::testCompile($node, $source, $environment); + } + + protected function getEngine() + { + return new MichelfMarkdownEngine(); + } + + public function getTests() + { + $tests = array(); + + $body = new \Twig_Node(array(new \Twig_Node_Text("#Title\n\nparagraph\n", 1))); + $node = new MarkdownNode($body, 1); + + $tests['simple text'] = array($node, <<env->getTokenParsers()->getTokenParser('markdown')->getEngine()->transform(\$content); +EOF + ); + + $body = new \Twig_Node(array(new \Twig_Node_Text(" #Title\n\n paragraph\n\n code\n", 1))); + $node = new MarkdownNode($body, 1); + + $tests['text with leading indent'] = array($node, <<env->getTokenParsers()->getTokenParser('markdown')->getEngine()->transform(\$content); +EOF + ); + + return $tests; + } +} diff --git a/app/vendor/asm89/twig-cache-extension/.gitignore b/app/vendor/asm89/twig-cache-extension/.gitignore new file mode 100644 index 000000000..8844fdc45 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/.gitignore @@ -0,0 +1,4 @@ +phpunit.xml +composer.lock +composer.phar +/vendor/ diff --git a/app/vendor/asm89/twig-cache-extension/.travis.yml b/app/vendor/asm89/twig-cache-extension/.travis.yml new file mode 100644 index 000000000..c9fed9788 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/.travis.yml @@ -0,0 +1,36 @@ +language: php + +cache: + directories: + - vendor + - $HOME/.composer/cache + +env: + - TWIG_VERSION="^1.0" + - TWIG_VERSION="^2.0" + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - hhvm + +matrix: + exclude: + - php: 5.3 + env: TWIG_VERSION="^2.0" + - php: 5.4 + env: TWIG_VERSION="^2.0" + - php: 5.5 + env: TWIG_VERSION="^2.0" + - php: 5.6 + env: TWIG_VERSION="^2.0" + - php: hhvm + env: TWIG_VERSION="^2.0" + +install: composer require twig/twig:${TWIG_VERSION} + +script: phpunit diff --git a/app/vendor/asm89/twig-cache-extension/LICENSE b/app/vendor/asm89/twig-cache-extension/LICENSE new file mode 100644 index 000000000..6e6b08857 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 Alexander + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/app/vendor/asm89/twig-cache-extension/README.md b/app/vendor/asm89/twig-cache-extension/README.md new file mode 100644 index 000000000..38c088788 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/README.md @@ -0,0 +1,238 @@ +Twig cache extension +==================== + +The missing cache extension for Twig. The extension allows for caching rendered parts of +templates using several cache strategies. + +[![Build Status](https://secure.travis-ci.org/asm89/twig-cache-extension.png?branch=master)](http://travis-ci.org/asm89/twig-cache-extension) + +## Installation + +The extension is installable via composer: + +```json +{ + "require": { + "asm89/twig-cache-extension": "~1.0" + } +} +``` + +## Quick start + +### Setup + +A minimal setup for adding the extension with the `LifeTimeCacheStrategy` and +doctrine array cache is as following: + +```php +addExtension($cacheExtension); +``` + +### Want to use a PSR-6 cache pool? + +Instead of using the default `DoctrineCacheAdapter` the extension also has +a `PSR-6` compatible adapter. You need to instantiate one of the cache pool +implementations as can be found on: http://php-cache.readthedocs.io/en/latest/ + +Example: Making use of the `ApcuCachePool` via the `PsrCacheAdapter`: + +```bash +composer require cache/apcu-adapter +``` + +```php +addExtension($cacheExtension); +``` + +### Usage + +To cache a part of a template in Twig surround the code with a `cache` block. +The cache block takes two parameters, first an "annotation" part, second the +"value" the cache strategy can work with. Example: + +```jinja +{% cache 'v1/summary' 900 %} + {# heavy lifting template stuff here, include/render other partials etc #} +{% endcache %} +``` + +Cache blocks can be nested: + +```jinja +{% cache 'v1' 900 %} + {% for item in items %} + {% cache 'v1' item %} + {# ... #} + {% endcache %} + {% endfor %} +{% endcache %} +``` + +The annotation can also be an expression: + +```jinja +{% set version = 42 %} +{% cache 'hello_v' ~ version 900 %} + Hello {{ name }}! +{% endcache %} +``` + +## Cache strategies + +The extension ships with a few cache strategies out of the box. Setup and usage +of all of them is described below. + +### Lifetime + +See the ["Quick start"](#quick-start) for usage information of the `LifetimeCacheStrategy`. + +### Generational + +Strategy for generational caching. + +In theory the strategy only saves fragments to the cache with infinite +lifetime. The key of the strategy lies in the fact that the keys for blocks +will change as the value for which the key is generated changes. + +For example: entities containing a last update time, would include a timestamp +in the key. For an interesting blog post about this type of caching see: +http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works + +### Blackhole + +Strategy for development mode. + +In development mode it often not very useful to cache fragments. The blackhole +strategy provides an easy way to not cache anything it all. It always generates +a new key and does not fetch or save any fragments. + +#### Setup + +In order to use the strategy you need to setup a `KeyGenerator` class that is +able to generate a cache key for a given value. + +The following naive example always assumes the value is an object with the methods +`getId()` and `getUpdatedAt()` method. The key then composed from the class +name, the id and the updated time of the object: + +```php +getId() . '_' . $value->getUpdatedAt(); + } + +} +``` + +Next the `GenerationalCacheStrategy` needs to be setup with the keygenerator. + +```php +addExtension($cacheExtension); +``` + +#### Usage + +The strategy expects an object as value for determining the cache key of the +block: + +```jinja +{% cache 'v1/summary' item %} + {# heavy lifting template stuff here, include/render other partials etc #} +{% endcache %} +``` + +### Using multiple strategies + +Different cache strategies are useful for different usecases. It is possible to +mix multiple strategies in an application with the +`IndexedChainingCacheStrategy`. The strategy takes an array of `'name' => +$strategy` and delegates the caching to the appropriate strategy. + +#### Setup + +```php + $lifetimeCacheStrategy, + 'gen' => $generationalCacheStrategy, +)); +$cacheExtension = new CacheExtension($cacheStrategy); + +$twig->addExtension($cacheExtension); +``` + +#### Usage + +The strategy expects an array with as key the name of the strategy which it +needs to delegate to and as value the appropriate value for the delegated +strategy. + +```jinja +{# delegate to lifetime strategy #} +{% cache 'v1/summary' {time: 300} %} + {# heavy lifting template stuff here, include/render other partials etc #} +{% endcache %} + +{# delegate to generational strategy #} +{% cache 'v1/summary' {gen: item} %} + {# heavy lifting template stuff here, include/render other partials etc #} +{% endcache %} +``` + +## Implementing a cache strategy + +Creating separate caches for different access levels, languages or other +usecases can be done by implementing a custom cache strategy. In order to do so +implement the `CacheProviderInterface`. It is recommended to use composition +and wrap a custom strategy around an existing one. + +## Authors + +Alexander + +## License + +twig-cache-extension is licensed under the MIT License - see the LICENSE file for details diff --git a/app/vendor/asm89/twig-cache-extension/composer.json b/app/vendor/asm89/twig-cache-extension/composer.json new file mode 100644 index 000000000..34db8cd46 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/composer.json @@ -0,0 +1,39 @@ +{ + "name": "asm89/twig-cache-extension", + "description": "Cache fragments of templates directly within Twig.", + "keywords": ["twig", "cache", "extension"], + "homepage": "https://github.com/asm89/twig-cache-extension", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Alexander", + "email": "iam.asm89@gmail.com" + } + ], + "require": { + "php": ">=5.3.2", + "twig/twig": "^1.0|^2.0" + }, + "require-dev": { + "doctrine/cache": "~1.0" + }, + "suggest": { + "psr/cache-implementation": "To make use of PSR-6 cache implementation via PsrCacheAdapter." + }, + "autoload": { + "psr-4": { + "": "lib/" + } + }, + "autoload-dev": { + "psr-4": { + "": "test/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + } +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheProvider/DoctrineCacheAdapter.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheProvider/DoctrineCacheAdapter.php new file mode 100644 index 000000000..bb627fa4e --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheProvider/DoctrineCacheAdapter.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\CacheProvider; + +use Asm89\Twig\CacheExtension\CacheProviderInterface; +use Doctrine\Common\Cache\Cache; + +/** + * Adapter class to use the cache classes provider by Doctrine. + * + * @author Alexander + */ +class DoctrineCacheAdapter implements CacheProviderInterface +{ + private $cache; + + /** + * @param Cache $cache + */ + public function __construct(Cache $cache) + { + $this->cache = $cache; + } + + /** + * {@inheritDoc} + */ + public function fetch($key) + { + return $this->cache->fetch($key); + } + + /** + * {@inheritDoc} + */ + public function save($key, $value, $lifetime = 0) + { + return $this->cache->save($key, $value, $lifetime); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheProvider/PsrCacheAdapter.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheProvider/PsrCacheAdapter.php new file mode 100644 index 000000000..3a525bc23 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheProvider/PsrCacheAdapter.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\CacheProvider; + +use Asm89\Twig\CacheExtension\CacheProviderInterface; +use Psr\Cache\CacheItemPoolInterface; + +/** + * Adapter class to make extension interoperable with every PSR-6 adapter. + * + * @see http://php-cache.readthedocs.io/ + * + * @author Rvanlaak + */ +class PsrCacheAdapter implements CacheProviderInterface +{ + /** + * @var CacheItemPoolInterface + */ + private $cache; + + /** + * @param CacheItemPoolInterface $cache + */ + public function __construct(CacheItemPoolInterface $cache) + { + $this->cache = $cache; + } + + /** + * @param string $key + * @return mixed|false + */ + public function fetch($key) + { + // PSR-6 implementation returns null, CacheProviderInterface expects false + $item = $this->cache->getItem($key); + if ($item->isHit()) { + return $item->get(); + } + return false; + } + + /** + * @param string $key + * @param string $value + * @param int|\DateInterval $lifetime + * @return bool + */ + public function save($key, $value, $lifetime = 0) + { + $item = $this->cache->getItem($key); + $item->set($value); + $item->expiresAfter($lifetime); + + return $this->cache->save($item); + } + +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheProviderInterface.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheProviderInterface.php new file mode 100644 index 000000000..7a0a56042 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheProviderInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension; + +/** + * Cache provider interface. + * + * @author Alexander + */ +interface CacheProviderInterface +{ + /** + * @param string $key + * + * @return mixed False, if there was no value to be fetched. Null or a string otherwise. + */ + public function fetch($key); + + /** + * @param string $key + * @param string $value + * @param integer $lifetime + * + * @return boolean + */ + public function save($key, $value, $lifetime = 0); +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/BlackholeCacheStrategy.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/BlackholeCacheStrategy.php new file mode 100644 index 000000000..fcc9445f7 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/BlackholeCacheStrategy.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\CacheStrategy; + +use Asm89\Twig\CacheExtension\CacheStrategyInterface; + +/** + * CacheStrategy which doesn't cache at all + * + * This strategy can be used in development mode, e.g. editing twig templates, + * to prevent previously cached versions from being rendered. + * + * @see https://github.com/asm89/twig-cache-extension/pull/29 + * + * @author Hagen Hübel + * + * @package Asm89\Twig\CacheExtension\CacheStrategy + */ +class BlackholeCacheStrategy implements CacheStrategyInterface +{ + /** + * {@inheritDoc} + */ + public function fetchBlock($key) + { + return false; + } + + /** + * {@inheritDoc} + */ + public function generateKey($annotation, $value) + { + return microtime(true) . mt_rand(); + } + + /** + * {@inheritDoc} + */ + public function saveBlock($key, $block) + { + // fire and forget + } +} \ No newline at end of file diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/GenerationalCacheStrategy.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/GenerationalCacheStrategy.php new file mode 100644 index 000000000..a67ea78bf --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/GenerationalCacheStrategy.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\CacheStrategy; + +use Asm89\Twig\CacheExtension\CacheProviderInterface; +use Asm89\Twig\CacheExtension\CacheStrategyInterface; +use Asm89\Twig\CacheExtension\Exception\InvalidCacheKeyException; + +/** + * Strategy for generational caching. + * + * In theory the strategy only saves fragments to the cache with infinite + * lifetime. The key of the strategy lies in the fact that the keys for blocks + * will change as the value for which the key is generated changes. + * + * For example: entities containing a last update time, would include a + * timestamp in the key. + * + * @see http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works + * + * @author Alexander + */ +class GenerationalCacheStrategy implements CacheStrategyInterface +{ + private $keyGenerator; + private $cache; + private $lifetime; + + /** + * @param CacheProviderInterface $cache + * @param KeyGeneratorInterface $keyGenerator + * @param integer $lifetime + */ + public function __construct(CacheProviderInterface $cache, KeyGeneratorInterface $keyGenerator, $lifetime = 0) + { + $this->keyGenerator = $keyGenerator; + $this->cache = $cache; + $this->lifetime = $lifetime; + } + + /** + * {@inheritDoc} + */ + public function fetchBlock($key) + { + return $this->cache->fetch($key); + } + + /** + * {@inheritDoc} + */ + public function generateKey($annotation, $value) + { + $key = $this->keyGenerator->generateKey($value); + + if (null === $key) { + throw new InvalidCacheKeyException(); + } + + return $annotation . '__GCS__' . $key; + } + + /** + * {@inheritDoc} + */ + public function saveBlock($key, $block) + { + return $this->cache->save($key, $block, $this->lifetime); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/IndexedChainingCacheStrategy.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/IndexedChainingCacheStrategy.php new file mode 100644 index 000000000..7305f04cf --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/IndexedChainingCacheStrategy.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\CacheStrategy; + +use Asm89\Twig\CacheExtension\CacheStrategyInterface; +use Asm89\Twig\CacheExtension\Exception\NonExistingStrategyException; +use Asm89\Twig\CacheExtension\Exception\NonExistingStrategyKeyException; + +/** + * Combines several configured cache strategies. + * + * Useful for combining for example generational cache strategy with a lifetime + * cache strategy, but also useful when combining several generational cache + * strategies which differ on cache lifetime (infinite, 1hr, 5m). + * + * @author Alexander + */ +class IndexedChainingCacheStrategy implements CacheStrategyInterface +{ + /** + * @var CacheStrategyInterface[] + */ + private $strategies; + + /** + * @param array $strategies + */ + public function __construct(array $strategies) + { + $this->strategies = $strategies; + } + + /** + * {@inheritDoc} + */ + public function fetchBlock($key) + { + return $this->strategies[$key['strategyKey']]->fetchBlock($key['key']); + } + + /** + * {@inheritDoc} + */ + public function generateKey($annotation, $value) + { + if (!is_array($value) || null === $strategyKey = key($value)) { + throw new NonExistingStrategyKeyException(); + } + + if (!isset($this->strategies[$strategyKey])) { + throw new NonExistingStrategyException($strategyKey); + } + + return array( + 'strategyKey' => $strategyKey, + 'key' => $this->strategies[$strategyKey]->generateKey($annotation, current($value)), + ); + } + + /** + * {@inheritDoc} + */ + public function saveBlock($key, $block) + { + return $this->strategies[$key['strategyKey']]->saveBlock($key['key'], $block); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/KeyGeneratorInterface.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/KeyGeneratorInterface.php new file mode 100644 index 000000000..ed9821080 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/KeyGeneratorInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\CacheStrategy; + +/** + * Generates a key for a given value. + * + * @author Alexander + */ +interface KeyGeneratorInterface +{ + /** + * Generate a cache key for a given value. + * + * @param mixed $value + * + * @return string + */ + public function generateKey($value); +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/LifetimeCacheStrategy.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/LifetimeCacheStrategy.php new file mode 100644 index 000000000..cb8351607 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategy/LifetimeCacheStrategy.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\CacheStrategy; + +use Asm89\Twig\CacheExtension\CacheProviderInterface; +use Asm89\Twig\CacheExtension\CacheStrategyInterface; +use Asm89\Twig\CacheExtension\Exception\InvalidCacheLifetimeException; + +/** + * Strategy for caching with a pre-defined lifetime. + * + * The value passed to the strategy is the lifetime of the cache block in + * seconds. + * + * @author Alexander + */ +class LifetimeCacheStrategy implements CacheStrategyInterface +{ + private $cache; + + /** + * @param CacheProviderInterface $cache + */ + public function __construct(CacheProviderInterface $cache) + { + $this->cache = $cache; + } + + /** + * {@inheritDoc} + */ + public function fetchBlock($key) + { + return $this->cache->fetch($key['key']); + } + + /** + * {@inheritDoc} + */ + public function generateKey($annotation, $value) + { + if (!is_numeric($value)) { + throw new InvalidCacheLifetimeException($value); + } + + return array( + 'lifetime' => $value, + 'key' => '__LCS__' . $annotation, + ); + } + + /** + * {@inheritDoc} + */ + public function saveBlock($key, $block) + { + return $this->cache->save($key['key'], $block, $key['lifetime']); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategyInterface.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategyInterface.php new file mode 100644 index 000000000..4a6e580a9 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/CacheStrategyInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension; + +/** + * Cache strategy interface. + * + * @author Alexander + */ +interface CacheStrategyInterface +{ + /** + * Fetch the block for a given key. + * + * @param mixed $key + * + * @return mixed + */ + public function fetchBlock($key); + + /** + * Generate a key for the value. + * + * @param string $annotation + * @param mixed $value + * + * @return mixed + */ + public function generateKey($annotation, $value); + + /** + * Save the contents of a rendered block. + * + * @param mixed $key + * @param string $block + * + * @return mixed + */ + public function saveBlock($key, $block); +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/BaseException.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/BaseException.php new file mode 100644 index 000000000..470b16122 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/BaseException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\Exception; + +/** + * @todo Replace \RuntimeException with \InvalidArgumentException at version 2.0 + */ +class BaseException extends \RuntimeException +{ + +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/InvalidCacheKeyException.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/InvalidCacheKeyException.php new file mode 100644 index 000000000..33275591e --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/InvalidCacheKeyException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\Exception; + +class InvalidCacheKeyException extends BaseException +{ + /** + * {@inheritdoc} + */ + public function __construct($message = 'Key generator did not return a key.', $code = 0, \Exception $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/InvalidCacheLifetimeException.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/InvalidCacheLifetimeException.php new file mode 100644 index 000000000..e78035529 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/InvalidCacheLifetimeException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\Exception; + +class InvalidCacheLifetimeException extends BaseException +{ + /** + * {@inheritdoc} + */ + public function __construct($lifetime, $code = 0, \Exception $previous = null) + { + parent::__construct(sprintf('Value "%s" is not a valid lifetime.', gettype($lifetime)), $code, $previous); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/NonExistingStrategyException.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/NonExistingStrategyException.php new file mode 100644 index 000000000..c4ea6f670 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/NonExistingStrategyException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\Exception; + +class NonExistingStrategyException extends BaseException +{ + /** + * {@inheritdoc} + */ + public function __construct($strategyKey, $code = 0, \Exception $previous = null) + { + parent::__construct(sprintf('No strategy configured with key "%s".', $strategyKey), $code, $previous); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/NonExistingStrategyKeyException.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/NonExistingStrategyKeyException.php new file mode 100644 index 000000000..eef30a7a4 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Exception/NonExistingStrategyKeyException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\Exception; + +class NonExistingStrategyKeyException extends BaseException +{ + /** + * {@inheritdoc} + */ + public function __construct($message = 'No strategy key found in value.', $code = 0, \Exception $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Extension.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Extension.php new file mode 100644 index 000000000..3a7ace884 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Extension.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension; + +/** + * Extension for caching template blocks with twig. + * + * @author Alexander + */ +class Extension extends \Twig_Extension +{ + private $cacheStrategy; + + /** + * @param CacheStrategyInterface $cacheStrategy + */ + public function __construct(CacheStrategyInterface $cacheStrategy) + { + $this->cacheStrategy = $cacheStrategy; + } + + /** + * @return CacheStrategyInterface + */ + public function getCacheStrategy() + { + return $this->cacheStrategy; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + if (version_compare(\Twig_Environment::VERSION, '1.26.0', '>=')) { + return get_class($this); + } + return 'asm89_cache'; + } + + /** + * {@inheritDoc} + */ + public function getTokenParsers() + { + return array( + new TokenParser\Cache(), + ); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Node/CacheNode.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Node/CacheNode.php new file mode 100644 index 000000000..25972890f --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/Node/CacheNode.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\Node; + +/** + * Cache twig node. + * + * @author Alexander + */ +class CacheNode extends \Twig_Node +{ + private static $cacheCount = 1; + + /** + * @param \Twig_Node_Expression $annotation + * @param \Twig_Node_Expression $keyInfo + * @param \Twig_NodeInterface $body + * @param integer $lineno + * @param string $tag + */ + public function __construct(\Twig_Node_Expression $annotation, \Twig_Node_Expression $keyInfo, \Twig_Node $body, $lineno, $tag = null) + { + parent::__construct(array('key_info' => $keyInfo, 'body' => $body, 'annotation' => $annotation), array(), $lineno, $tag); + } + + /** + * {@inheritDoc} + */ + public function compile(\Twig_Compiler $compiler) + { + $i = self::$cacheCount++; + + if (version_compare(\Twig_Environment::VERSION, '1.26.0', '>=')) { + $extension = 'Asm89\Twig\CacheExtension\Extension'; + } else { + $extension = 'asm89_cache'; + } + + $compiler + ->addDebugInfo($this) + ->write("\$asm89CacheStrategy".$i." = \$this->env->getExtension('{$extension}')->getCacheStrategy();\n") + ->write("\$asm89Key".$i." = \$asm89CacheStrategy".$i."->generateKey(") + ->subcompile($this->getNode('annotation')) + ->raw(", ") + ->subcompile($this->getNode('key_info')) + ->write(");\n") + ->write("\$asm89CacheBody".$i." = \$asm89CacheStrategy".$i."->fetchBlock(\$asm89Key".$i.");\n") + ->write("if (\$asm89CacheBody".$i." === false) {\n") + ->indent() + ->write("ob_start();\n") + ->indent() + ->subcompile($this->getNode('body')) + ->outdent() + ->write("\n") + ->write("\$asm89CacheBody".$i." = ob_get_clean();\n") + ->write("\$asm89CacheStrategy".$i."->saveBlock(\$asm89Key".$i.", \$asm89CacheBody".$i.");\n") + ->outdent() + ->write("}\n") + ->write("echo \$asm89CacheBody".$i.";\n") + ; + } +} diff --git a/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/TokenParser/Cache.php b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/TokenParser/Cache.php new file mode 100644 index 000000000..3d45a661f --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/lib/Asm89/Twig/CacheExtension/TokenParser/Cache.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\TokenParser; + +use Asm89\Twig\CacheExtension\Node\CacheNode; + +/** + * Parser for cache/endcache blocks. + * + * @author Alexander + */ +class Cache extends \Twig_TokenParser +{ + /** + * @param \Twig_Token $token + * + * @return boolean + */ + public function decideCacheEnd(\Twig_Token $token) + { + return $token->test('endcache'); + } + + /** + * {@inheritDoc} + */ + public function getTag() + { + return 'cache'; + } + + /** + * {@inheritDoc} + */ + public function parse(\Twig_Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + + $annotation = $this->parser->getExpressionParser()->parseExpression(); + + $key = $this->parser->getExpressionParser()->parseExpression(); + + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $body = $this->parser->subparse(array($this, 'decideCacheEnd'), true); + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + + return new CacheNode($annotation, $key, $body, $lineno, $this->getTag()); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/phpunit.xml.dist b/app/vendor/asm89/twig-cache-extension/phpunit.xml.dist new file mode 100644 index 000000000..888e3058d --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + ./test/Asm89/ + + + + + + ./lib/ + + + diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheProvider/DoctrineCacheAdapterTest.php b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheProvider/DoctrineCacheAdapterTest.php new file mode 100644 index 000000000..4642ec058 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheProvider/DoctrineCacheAdapterTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\Tests\CacheProvider; + +use Asm89\Twig\CacheExtension\CacheProvider\DoctrineCacheAdapter; + +class DoctrineCacheAdapterTest extends \PHPUnit_Framework_TestCase +{ + public function testFetch() + { + $cacheMock = $this->createCacheMock(); + $cacheMock->expects($this->any()) + ->method('fetch') + ->will($this->returnValue('fromcache')); + + $cache = new DoctrineCacheAdapter($cacheMock); + + $this->assertEquals('fromcache', $cache->fetch('test')); + } + + public function testSave() + { + $cacheMock = $this->createCacheMock(); + $cacheMock->expects($this->once()) + ->method('save') + ->with('key', 'value', 42); + + $cache = new DoctrineCacheAdapter($cacheMock); + + $cache->save('key', 'value', 42); + } + + public function createCacheMock() + { + return $this->getMock('Doctrine\Common\Cache\Cache'); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheStrategy/GenerationCacheStrategyTest.php b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheStrategy/GenerationCacheStrategyTest.php new file mode 100644 index 000000000..5084d219d --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheStrategy/GenerationCacheStrategyTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\Tests\CacheStrategy; + +use Asm89\Twig\CacheExtension\CacheStrategy\GenerationalCacheStrategy; + +class GenerationalCacheStrategyTest extends \PHPUnit_Framework_TestCase +{ + private $keyGeneratorMock; + private $cacheProviderMock; + + public function createCacheStrategy($lifetime = 0) + { + $this->keyGeneratorMock = $this->createKeyGeneratorMock(); + $this->cacheProviderMock = $this->createCacheProviderMock(); + + return new GenerationalCacheStrategy($this->cacheProviderMock, $this->keyGeneratorMock, $lifetime); + } + + public function testGenerateKeyContainsAnnotation() + { + $strategy = $this->createCacheStrategy(); + $this->keyGeneratorMock->expects($this->any()) + ->method('generateKey') + ->will($this->returnValue('foo')); + + $this->assertEquals('v42__GCS__foo', $strategy->generateKey('v42', 'value')); + } + + /** + * @expectedException \Asm89\Twig\CacheExtension\Exception\InvalidCacheKeyException + */ + public function testGenerationKeyThrowsExceptionWhenKeyGeneratorReturnsNull() + { + $strategy = $this->createCacheStrategy(); + + $strategy->generateKey('v42', 'value'); + } + + /** + * @dataProvider getLifeTimes + */ + public function testSaveBlockUsesConfiguredLifetime($lifetime) + { + $strategy = $this->createCacheStrategy($lifetime); + $this->cacheProviderMock->expects($this->any()) + ->method('save') + ->with('key', 'value', $lifetime) + ->will($this->returnValue('foo')); + + $strategy->saveBlock('key', 'value'); + } + + public function getLifetimes() + { + return array( + array(0), + array(60), + ); + } + + public function createKeyGeneratorMock() + { + return $this->getMock('Asm89\Twig\CacheExtension\CacheStrategy\KeyGeneratorInterface'); + } + + public function createCacheProviderMock() + { + return $this->getMock('Asm89\Twig\CacheExtension\CacheProviderInterface'); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheStrategy/IndexedChainingCacheStrategyTest.php b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheStrategy/IndexedChainingCacheStrategyTest.php new file mode 100644 index 000000000..3a68a09f5 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheStrategy/IndexedChainingCacheStrategyTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\Tests\CacheStrategy; + +use Asm89\Twig\CacheExtension\CacheStrategy\IndexedChainingCacheStrategy; + +class IndexedChainingCacheStrategyTest extends \PHPUnit_Framework_TestCase +{ + private $cacheStrategyMocks = array(); + + public function createCacheStrategy() + { + foreach($this->getStrategies() as $config) { + list($key, $return) = $config; + + $cacheStrategyMock = $this->createCacheStrategyMock(); + $cacheStrategyMock->expects($this->any()) + ->method('generateKey') + ->will($this->returnValue($return)); + + $this->cacheStrategyMocks[$key] = $cacheStrategyMock; + } + + return new IndexedChainingCacheStrategy($this->cacheStrategyMocks); + } + + /** + * @dataProvider getStrategies + */ + public function testGenerateKeyProxiesToAppropriateStrategy($key, $return) + { + $strategy = $this->createCacheStrategy(); + + $generatedKey = $strategy->generateKey('v42', array($key => 'proxied_value')); + + $this->assertEquals($return, $generatedKey['key']); + $this->assertEquals($key, $generatedKey['strategyKey']); + } + + /** + * @expectedException \Asm89\Twig\CacheExtension\Exception\NonExistingStrategyKeyException + */ + public function testGenerateKeyThrowsExceptionOnMissingKey() + { + $strategy = $this->createCacheStrategy(); + $strategy->generateKey('v42', 'proxied_value'); + } + + /** + * @expectedException \Asm89\Twig\CacheExtension\Exception\NonExistingStrategyException + * @expectedExceptionMessage No strategy configured with key "unknown" + */ + public function testGenerateKeyThrowsExceptionOnUnknownKey() + { + $strategy = $this->createCacheStrategy(); + $strategy->generateKey('v42', array('unknown' => 'proxied_value')); + } + + public function getStrategies() + { + return array( + array('foo', 'foo_key'), + array('bar', 'bar_key'), + ); + } + + public function createCacheStrategyMock() + { + return $this->getMock('Asm89\Twig\CacheExtension\CacheStrategyInterface'); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheStrategy/LifetimeCacheStrategyTest.php b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheStrategy/LifetimeCacheStrategyTest.php new file mode 100644 index 000000000..48905e424 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/CacheStrategy/LifetimeCacheStrategyTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\Tests\CacheStrategy; + +use Asm89\Twig\CacheExtension\CacheStrategy\LifetimeCacheStrategy; + +class LifetimeCacheStrategyTest extends \PHPUnit_Framework_TestCase +{ + private $cacheProviderMock; + + public function createCacheStrategy() + { + $this->cacheProviderMock = $this->createCacheProviderMock(); + + return new LifetimeCacheStrategy($this->cacheProviderMock); + } + + public function testGenerateKeyUsesGivenLifetime() + { + $strategy = $this->createCacheStrategy(); + + $key = $strategy->generateKey('v42', 42); + + $this->assertEquals(42, $key['lifetime']); + } + + public function testGenerateKeyAnnotatesKey() + { + $strategy = $this->createCacheStrategy(); + + $key = $strategy->generateKey('the_annotation', 42); + + $this->assertContains('the_annotation', $key['key']); + } + + /** + * @dataProvider getInvalidLifetimeValues + * @expectedException \Asm89\Twig\CacheExtension\Exception\InvalidCacheLifetimeException + */ + public function testGenerateKeyThrowsExceptionWhenNoLifetimeProvided($value) + { + $strategy = $this->createCacheStrategy(); + + $strategy->generateKey('v42', $value); + } + + public function getInvalidLifetimeValues() + { + return array( + array('foo'), + array(array('foo')), + ); + } + + public function createCacheProviderMock() + { + return $this->getMock('Asm89\Twig\CacheExtension\CacheProviderInterface'); + } +} diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/FunctionalExtensionTest.php b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/FunctionalExtensionTest.php new file mode 100644 index 000000000..77a58e65a --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/FunctionalExtensionTest.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Asm89\Twig\CacheExtension\Tests; + +use Asm89\Twig\CacheExtension\CacheProvider\DoctrineCacheAdapter; +use Asm89\Twig\CacheExtension\CacheStrategy\KeyGeneratorInterface; +use Asm89\Twig\CacheExtension\CacheStrategy\GenerationalCacheStrategy; +use Asm89\Twig\CacheExtension\CacheStrategy\IndexedChainingCacheStrategy; +use Asm89\Twig\CacheExtension\CacheStrategy\LifetimeCacheStrategy; +use Asm89\Twig\CacheExtension\Extension; +use Doctrine\Common\Cache\ArrayCache; +use Twig_Loader_Filesystem; +use Twig_Environment; + +class FunctionalExtensionTest extends \PHPUnit_Framework_TestCase +{ + private $cache; + + protected function createCacheProvider() + { + $this->cache = new ArrayCache(); + + return new DoctrineCacheAdapter($this->cache); + } + + protected function createCacheStrategy($name = null) + { + $cacheProvider = $this->createCacheProvider(); + $keyGenerator = $this->createKeyGenerator(); + $lifetime = 0; + + switch ($name) { + case 'time': + return new LifetimeCacheStrategy($cacheProvider); + case 'indexed': + return new IndexedChainingCacheStrategy(array( + 'gcs' => new GenerationalCacheStrategy($cacheProvider, $keyGenerator, $lifetime), + 'time' => new LifetimeCacheStrategy($cacheProvider), + )); + default: + return new GenerationalCacheStrategy($cacheProvider, $keyGenerator, $lifetime); + } + } + + protected function createKeyGenerator() + { + return new KeyGenerator(); + } + + protected function createTwig($cacheStrategyName = null) + { + $loader = new Twig_Loader_Filesystem(__DIR__ . '/fixtures/'); + $twig = new Twig_Environment($loader); + + $cacheExtension = new Extension($this->createCacheStrategy($cacheStrategyName)); + + $twig->addExtension($cacheExtension); + + return $twig; + } + + protected function getValue($value, $updatedAt) + { + return new Value($value, $updatedAt); + } + + public function testCachesWithSameCacheKey() + { + $twig = $this->createTwig(); + + $rendered = $twig->render('gcs_value.twig', array('value' => $this->getValue('asm89', 1))); + $this->assertEquals('Hello asm89!', $rendered); + + $rendered2 = $twig->render('gcs_value.twig', array('value' => $this->getValue('fabpot', 1))); + $this->assertEquals('Hello asm89!', $rendered2); + } + + public function testDifferentCacheOnDifferentAnnotation() + { + $twig = $this->createTwig(); + + $rendered = $twig->render('gcs_value.twig', array('value' => $this->getValue('asm89', 1))); + $this->assertEquals('Hello asm89!', $rendered); + + $rendered2 = $twig->render('gcs_value.twig', array('value' => $this->getValue('fabpot', 1))); + $this->assertEquals('Hello asm89!', $rendered2); + + $rendered3 = $twig->render('gcs_value_v2.twig', array('value' => $this->getValue('fabpot', 1))); + $this->assertEquals('Hello fabpot!', $rendered3); + } + + public function testLifetimeCacheStrategy() + { + $twig = $this->createTwig('time'); + + $rendered = $twig->render('lcs_value.twig', array('value' => $this->getValue('asm89', 1))); + $this->assertEquals('Hello asm89!', $rendered); + + $rendered2 = $twig->render('lcs_value.twig', array('value' => $this->getValue('fabpot', 1))); + $this->assertEquals('Hello asm89!', $rendered2); + + $this->cache->flushAll(); + + $rendered3 = $twig->render('lcs_value.twig', array('value' => $this->getValue('fabpot', 1))); + $this->assertEquals('Hello fabpot!', $rendered3); + } + + public function testIndexedChainingStrategy() + { + $twig = $this->createTwig('indexed'); + + $rendered = $twig->render('ics_value.twig', array('value' => $this->getValue('asm89', 1))); + $this->assertEquals('Hello asm89!', $rendered); + + $rendered2 = $twig->render('ics_value.twig', array('value' => $this->getValue('fabpot', 1))); + $this->assertEquals('Hello asm89!', $rendered2); + + $this->cache->flushAll(); + + $rendered3 = $twig->render('ics_value.twig', array('value' => $this->getValue('fabpot', 1))); + $this->assertEquals('Hello fabpot!', $rendered3); + } + + /** + * @expectedException Twig_Error_Runtime + * @expectedExceptionMessage An exception has been thrown during the rendering of a template ("No strategy key found in value.") + */ + public function testIndexedChainingStrategyNeedsKey() + { + $twig = $this->createTwig('indexed'); + + $twig->render('ics_no_key.twig', array('value' => $this->getValue('asm89', 1))); + } + + public function testAnnotationExpression() + { + $twig = $this->createTwig('indexed'); + + $rendered = $twig->render('annotation_expression.twig', array('value' => $this->getValue('asm89', 1), 'version' => 1)); + $this->assertEquals('Hello asm89!Hello asm89!', $rendered); + } +} + +class KeyGenerator implements KeyGeneratorInterface +{ + public function generateKey($value) + { + return get_class($value) . '_' . $value->getUpdatedAt(); + } + +} + +class Value +{ + private $value; + private $updatedAt; + + public function __construct($value, $updatedAt) + { + $this->value = $value; + $this->updatedAt = $updatedAt; + } + + public function getUpdatedAt() + { + return $this->updatedAt; + } + + public function __toString() + { + return $this->value; + } +} diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/annotation_expression.twig b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/annotation_expression.twig new file mode 100644 index 000000000..8f4587b79 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/annotation_expression.twig @@ -0,0 +1,3 @@ +{%- set k = "v'" ~ version -%} +{%- cache k {time: 10} -%}Hello {{ value }}!{%- endcache -%} +{%- cache 'bob'|capitalize {gcs: value} %}Hello {{ value }}!{%- endcache -%} diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/annotation_not_string.twig b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/annotation_not_string.twig new file mode 100644 index 000000000..d962dedc3 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/annotation_not_string.twig @@ -0,0 +1,2 @@ +{% set annotation = 'v1' %} +{% cache annotation value %}Hello {{ value }}!{% endcache %} diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/gcs_value.twig b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/gcs_value.twig new file mode 100644 index 000000000..59a39bf2e --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/gcs_value.twig @@ -0,0 +1 @@ +{% cache 'v1' value %}Hello {{ value }}!{% endcache %} diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/gcs_value_v2.twig b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/gcs_value_v2.twig new file mode 100644 index 000000000..e7281174f --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/gcs_value_v2.twig @@ -0,0 +1 @@ +{% cache 'v2' value %}Hello {{ value }}!{% endcache %} diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/ics_no_key.twig b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/ics_no_key.twig new file mode 100644 index 000000000..12b924b07 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/ics_no_key.twig @@ -0,0 +1 @@ +{% cache 'v1' 10 %}Hello {{ value }}!{% endcache %} diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/ics_value.twig b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/ics_value.twig new file mode 100644 index 000000000..efc52ea68 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/ics_value.twig @@ -0,0 +1 @@ +{% cache 'v1' {time: 10} %}Hello {{ value }}!{% endcache %} diff --git a/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/lcs_value.twig b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/lcs_value.twig new file mode 100644 index 000000000..12b924b07 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/Asm89/Twig/CacheExtension/Tests/fixtures/lcs_value.twig @@ -0,0 +1 @@ +{% cache 'v1' 10 %}Hello {{ value }}!{% endcache %} diff --git a/app/vendor/asm89/twig-cache-extension/test/bootstrap.php b/app/vendor/asm89/twig-cache-extension/test/bootstrap.php new file mode 100644 index 000000000..d45671926 --- /dev/null +++ b/app/vendor/asm89/twig-cache-extension/test/bootstrap.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (file_exists($file = __DIR__.'/../vendor/autoload.php')) { + $autoload = require_once $file; +} else { + throw new RuntimeException('Install dependencies to run test suite.'); +} diff --git a/app/vendor/aura/intl/.gitignore b/app/vendor/aura/intl/.gitignore new file mode 100644 index 000000000..75d706bcb --- /dev/null +++ b/app/vendor/aura/intl/.gitignore @@ -0,0 +1,3 @@ +/tests/tmp +/vendor +/composer.lock diff --git a/app/vendor/aura/intl/.scrutinizer.yml b/app/vendor/aura/intl/.scrutinizer.yml new file mode 100644 index 000000000..7c7c84543 --- /dev/null +++ b/app/vendor/aura/intl/.scrutinizer.yml @@ -0,0 +1,10 @@ +filter: + paths: ["src/*"] +tools: + external_code_coverage: true + php_code_coverage: true + php_sim: true + php_mess_detector: true + php_pdepend: true + php_analyzer: true + php_cpd: true diff --git a/app/vendor/aura/intl/.travis.yml b/app/vendor/aura/intl/.travis.yml new file mode 100644 index 000000000..823c70497 --- /dev/null +++ b/app/vendor/aura/intl/.travis.yml @@ -0,0 +1,14 @@ +sudo: false +language: php +php: + - 5.6 + - hhvm + - 7 +before_script: + - composer self-update + - composer install +script: + - phpunit --coverage-clover=coverage.clover +after_script: + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover coverage.clover diff --git a/app/vendor/aura/intl/CHANGELOG.md b/app/vendor/aura/intl/CHANGELOG.md new file mode 100644 index 000000000..12e89902c --- /dev/null +++ b/app/vendor/aura/intl/CHANGELOG.md @@ -0,0 +1,66 @@ +# CHANGELOG + +## 3.0.0 + +- Final release. No changes after first beta. + +## 3.0.0-beta1 + +- Removed `aura/installer-default` from composer.json which was used by aura framework version 1 to install in package folder. +- Fixes [issue 17](https://github.com/auraphp/Aura.Intl/issues/17) by removing Aura.Di wiring trait tests. +- Changed directory structure from PSR-0 to PSR-4. +- Supported PHP version : 5.6+. +- There is no other BC breaks from 1.x version + + +## 1.1.1 + +Hygiene release: Composer update. + +## 1.1.0 + +- Merge pull request #11 from lorenzo/feature/format-key. If Translator::translate() cannot translate, it now returns the incoming translation key as the translated message. This makes it easier to use the key as a human readable string, which can contain formatting placeholders. With this feature the developer will be able to see immediate feedback of their translation messages without having to translate to a specific locale beforehand. + +## 1.0.1 + +Hygiene release. + +- Merge pull request #10 from harikt/fixdoc; fixes #9, add a test for failure. The expected exception need to be fixed. + +- Merge pull request #8 from harikt/v2config, adding v2 config files. + +- Merge pull request #7 from harikt/issue6, add a test to show it works. Thank you @samdark. + +- Point travis status badge to develop branch + +## 1.0.0 + +- [CHG] When interpolating array values into strings, now converts the array + to comma-separated values instead of printing 'Array'. + +- [DOC] PHP 5.5 appears to be able to format strings with missing token + replacements; updated tests to account for this. + +## 1.0.0-beta2 + +- [FIX] Multiple typo fixes; thanks, @pborreli. + +- [CHG] Use {foo} instead of {:foo} in line with PSR-3 placeholders. + +- [NEW] Add a PackageFactory + +- [NEW] Add more-detailed exception classes + +- [ADD] In config, add 'intl_package_factory' as a service + +- [CHG] Throw IcuVersionTooLow Exception if IntlFormatter is instantiated with + ICU Version lower than 4.8; Skip all IntlFormatter tests if the intl + extension is not loaded. Thanks, @mapthegod. + +- [CHG] In the FormatterLocator, registry entries *must* be wrapped in a + callable from now on. + + +## 1.0.0-beta1 + +Initial release. diff --git a/app/vendor/aura/intl/CONTRIBUTING.md b/app/vendor/aura/intl/CONTRIBUTING.md new file mode 100644 index 000000000..b68f9b85c --- /dev/null +++ b/app/vendor/aura/intl/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Contributing + +We are happy to review any contributions you want to make. When contributing, please follow the rules outlined at . + +The time between submitting a contribution and its review one may be extensive; do not be discouraged if there is not immediate feedback. + +Thanks! diff --git a/app/vendor/aura/intl/LICENSE b/app/vendor/aura/intl/LICENSE new file mode 100644 index 000000000..5f5f3b61c --- /dev/null +++ b/app/vendor/aura/intl/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017, Aura for PHP + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/app/vendor/aura/intl/README.md b/app/vendor/aura/intl/README.md new file mode 100644 index 000000000..0537d4721 --- /dev/null +++ b/app/vendor/aura/intl/README.md @@ -0,0 +1,59 @@ +# Aura.Intl + +The Aura.Intl package provides internationalization (I18N) tools, specifically +package-oriented per-locale message translation. + +## Installation and Autoloading + +This package is installable and PSR-4 autoloadable via Composer as +[aura/intl][]. + +Alternatively, [download a release][], or clone this repository, then map the +`Aura\Intl\` namespace to the package `src/` directory. + +## Dependencies + +This package requires PHP 5.6 or later; it has been tested on PHP 5.6, 7.0 +and HHVM. We recommend using the latest available version of PHP as a matter of +principle. + +Aura library packages may sometimes depend on external interfaces, but never on +external implementations. This allows compliance with community standards +without compromising flexibility. For specifics, please examine the package +[composer.json][] file. + +## Quality + +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/auraphp/Aura.Intl/badges/quality-score.png?b=3.x)](https://scrutinizer-ci.com/g/auraphp/Aura.Intl/) +[![Code Coverage](https://scrutinizer-ci.com/g/auraphp/Aura.Intl/badges/coverage.png?b=3.x)](https://scrutinizer-ci.com/g/auraphp/Aura.Intl/) +[![Build Status](https://travis-ci.org/auraphp/Aura.Intl.png?branch=3.x)](https://travis-ci.org/auraphp/Aura.Intl) + +This project adheres to [Semantic Versioning](http://semver.org/). + +To run the unit tests at the command line, issue `composer install` and then +`phpunit` at the package root. This requires [Composer][] to be available as +`composer`, and [PHPUnit][] to be available as `phpunit`. + +This package attempts to comply with [PSR-1][], [PSR-2][], and [PSR-4][]. If +you notice compliance oversights, please send a patch via pull request. + +## Community + +To ask questions, provide feedback, or otherwise communicate with other Aura +users, please join our [Google Group][], follow [@auraphp][], or chat with us +on Freenode in the #auraphp channel. + +## Documentation + +This package is fully documented [here](./docs/index.md). + +[PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md +[PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md +[PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md +[Composer]: http://getcomposer.org/ +[PHPUnit]: http://phpunit.de/ +[Google Group]: http://groups.google.com/group/auraphp +[@auraphp]: http://twitter.com/auraphp +[download a release]: https://github.com/auraphp/Aura.Intl/releases +[aura/intl]: https://packagist.org/packages/aura/intl +[composer.json]: ./composer.json diff --git a/app/vendor/aura/intl/composer.json b/app/vendor/aura/intl/composer.json new file mode 100644 index 000000000..6366d0ab7 --- /dev/null +++ b/app/vendor/aura/intl/composer.json @@ -0,0 +1,35 @@ +{ + "name": "aura/intl", + "type": "library", + "description": "The Aura Intl package provides internationalization tools, specifically message translation.", + "keywords": [ + "intl", + "internationalization", + "i18n", + "l10n", + "localization", + "globalization", + "g11n" + ], + "homepage": "https://github.com/auraphp/Aura.Intl", + "license": "MIT", + "authors": [ + { + "name": "Aura.Intl Contributors", + "homepage": "https://github.com/auraphp/Aura.Intl/contributors" + } + ], + "require": { + "php": "^5.6|^7.0" + }, + "autoload": { + "psr-4": { + "Aura\\Intl\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Aura\\Intl\\": "tests/" + } + } +} diff --git a/app/vendor/aura/intl/docs/index.md b/app/vendor/aura/intl/docs/index.md new file mode 100644 index 000000000..f097c5fac --- /dev/null +++ b/app/vendor/aura/intl/docs/index.md @@ -0,0 +1,218 @@ +# Aura.Intl + +The Aura.Intl package provides internationalization (I18N) tools, specifically +package-oriented per-locale message translation. + +## Getting Started + +You can instantiate _TranslatorLocator_ object from _TranslatorLocatorFactory_ +as below + +```php +newInstance(); +?> +``` + +Alternatively, we can add the Aura.Intl package `/path/to/Aura.Intl/src` to +our autoloader and build a translator locator manually: + +```php + function () { return new \Aura\Intl\BasicFormatter; }, + 'intl' => function () { return new \Aura\Intl\IntlFormatter; }, + ]), + new TranslatorFactory, + 'en_US' +); +?> +``` + +## Setting Localized Messages For A Package + +We can set localized messages for a package through the `PackageLocator` object +from the translator locator. We create a new `Package` with messages and place +it into the locator as a callable. The messages take the form of a message key and +and message string. + +```php +getPackages(); + +// place into the locator for Vendor.Package +$packages->set('Vendor.Package', 'en_US', function() { + // create a US English message set + $package = new Package; + $package->setMessages([ + 'FOO' => 'The text for "foo."', + 'BAR' => 'The text for "bar."', + ]); + return $package; +}); + +// place into the locator for a Vendor.Package +$packages->set('Vendor.Package', 'pt_BR', function() { + // a Brazilian Portuguese message set + $package = new Package; + $package->setMessages([ + 'FOO' => 'O texto de "foo".', + 'BAR' => 'O texto de "bar".', + ]); + return $package; +}); +?> +``` + + +## Setting The Default Locale + +We can set the default locale for translations using the `setLocale()` method: + +```php +setLocale('pt_BR'); +?> +``` + +## Getting A Localized Message + +Now that the translator locator has messages and a default locale, we can get +an individual package translator. The package translator is suitable for +injection into another class, or for standalone use. + +```php +get('Vendor.Package'); +echo $translator->translate('FOO'); // 'O texto de "foo".' +?> +``` + +You can get a translator for a non-default locale as well: + +```php +get('Vendor.Package', 'en_US'); +echo $translator->translate('FOO'); // 'The text for "foo."' +?> +``` + + +## Replacing Message Tokens With Values + +We often need to use dynamic values in translated messages. First, the +message string needs to have a token placeholder for the dynamic value: + +```php +getPackages(); + +$packages->set('Vendor.Dynamic', 'en_US', function() { + + // US English messages + $package = new Package; + $package->setMessages([ + 'PAGE' => 'Page {page} of {pages} pages.', + ]); + return $package; +}); + +$packages->set('Vendor.Dynamic', 'pt_BR', function() { + // Brazilian Portuguese messages + $package = new Package; + $package->setMessages([ + 'PAGE' => 'Página {page} de {pages} páginas.', + ]); + return $package; +}); +?> +``` + +Then, when we translate the message, we provide an array of tokens and +replacement values. These will be interpolated into the message string. + +```php +get('Vendor.Dynamic'); +echo $translator->translate('PAGE', [ + 'page' => 1, + 'pages' => 1, +]); // 'Página 1 de 1 páginas.' +?> +``` + +## Pluralized Messages + +Usually, we need to use different messages when a value is singular or plural. +The `BasicFormatter` is not capable of presenting different messages based on +different token values. The `IntlFormatter` *is* capable, but the PHP +[`intl`](http://php.net/intl) extension must be loaded to take advantage of +it, and we must specify the `'intl'` formatter for the package in the catalog. + +When using the `IntlFormatter`, we can build our message strings to present +singular or plural messages, as in the following example: + +```php +getCatalog(); + +// get the Vendor.Dynamic package en_US locale and set +// US English messages with pluralization. note the use +// of # instead of {pages} herein; using the placeholder +// "inside itself" with the Intl formatter causes trouble. +$package->setMessages([ + 'PAGE' => '{pages,plural,' + . '=0{No pages.}' + . '=1{One page only.}' + . 'other{Page {page} of # pages.}' + . '}' +]); + +// use the 'intl' formatter for this package and locale +$package->setFormatter('intl'); + +// now that we have added the pluralizable messages, +// get the US English translator for the package +$translator = $translators->get('Vendor.Dynamic', 'en_US'); + +// zero translation +echo $translator->translate('PAGE', [ + 'page' => 0, + 'pages' => 0, +]); // 'No pages.' + +// singular translation +echo $translator->translate('PAGE', [ + 'page' => 1, + 'pages' => 1, +]); // 'One page only.' + +// plural translation +echo $translator->translate('PAGE', [ + 'page' => 3, + 'pages' => 10, +]); // 'Page 3 of 10 pages.' +?> +``` + +Note that you can use other tokens within a pluralized token string to build +more complex messages. For more information, see the following: + + diff --git a/app/vendor/aura/intl/phpunit.php b/app/vendor/aura/intl/phpunit.php new file mode 100644 index 000000000..ceaada7da --- /dev/null +++ b/app/vendor/aura/intl/phpunit.php @@ -0,0 +1,9 @@ + + + + ./tests + + + + + ./src/ + + + diff --git a/app/vendor/aura/intl/src/BasicFormatter.php b/app/vendor/aura/intl/src/BasicFormatter.php new file mode 100644 index 000000000..2d02a778f --- /dev/null +++ b/app/vendor/aura/intl/src/BasicFormatter.php @@ -0,0 +1,47 @@ + $value) { + // convert an array to a CSV string + if (is_array($value)) { + $value = '"' . implode('", "', $value) . '"'; + } + $replace['{' . $token . '}'] = $value; + } + return strtr($string, $replace); + } +} diff --git a/app/vendor/aura/intl/src/Exception.php b/app/vendor/aura/intl/src/Exception.php new file mode 100644 index 000000000..c86980522 --- /dev/null +++ b/app/vendor/aura/intl/src/Exception.php @@ -0,0 +1,22 @@ + $spec) { + $this->set($name, $spec); + } + } + + /** + * + * Sets a formatter into the registry by name. + * + * @param string $name The formatter name. + * + * @param callable $spec A callable that returns a formatter object. + * + * @return void + * + */ + public function set($name, $spec) + { + $this->registry[$name] = $spec; + $this->converted[$name] = false; + } + + /** + * + * Gets a formatter from the registry by name. + * + * @param string $name The formatter to retrieve. + * + * @return FormatterInterface A formatter object. + * + * @throws Exception\FormatterNotMapped + * + */ + public function get($name) + { + if (! isset($this->registry[$name])) { + throw new Exception\FormatterNotMapped($name); + } + + if (! $this->converted[$name]) { + $func = $this->registry[$name]; + $this->registry[$name] = $func(); + $this->converted[$name] = true; + } + + return $this->registry[$name]; + } +} diff --git a/app/vendor/aura/intl/src/IntlFormatter.php b/app/vendor/aura/intl/src/IntlFormatter.php new file mode 100644 index 000000000..719d6f94d --- /dev/null +++ b/app/vendor/aura/intl/src/IntlFormatter.php @@ -0,0 +1,98 @@ + $value) { + // convert an array to a CSV string + if (is_array($value)) { + $value = '"' . implode('", "', $value) . '"'; + } + + $values[$token] = $value; + } + + try { + $formatter = new MessageFormatter($locale, $string); + if (! $formatter) { + $this->throwCannotInstantiateFormatter(); + } + } catch (\Exception $e) { + $this->throwCannotInstantiateFormatter(); + } + + $result = $formatter->format($values); + if ($result === false) { + throw new Exception\CannotFormat( + $formatter->getErrorMessage(), + $formatter->getErrorCode() + ); + } + + return $result; + } + + /** + * + * Throws exception + * + * @throws Exception\CannotInstantiateFormatter + */ + protected function throwCannotInstantiateFormatter() + { + throw new Exception\CannotInstantiateFormatter( + intl_get_error_message(), + intl_get_error_code() + ); + } +} diff --git a/app/vendor/aura/intl/src/Package.php b/app/vendor/aura/intl/src/Package.php new file mode 100644 index 000000000..b714e8325 --- /dev/null +++ b/app/vendor/aura/intl/src/Package.php @@ -0,0 +1,197 @@ +formatter = $formatter; + $this->fallback = $fallback; + $this->messages = $messages; + } + + /** + * + * Sets the messages for this package. + * + * @param array $messages The messages for this package. + * + * @return void + * + */ + public function setMessages(array $messages) + { + $this->messages = $messages; + } + + /** + * + * Adds one message for this package. + * + * @param string $key the key of the message + * + * @param string $message the actual message + * + * @return void + * + */ + public function addMessage($key, $message) + { + $this->messages[$key] = $message; + } + + /** + * + * Adds new messages for this package. + * + * @param array $messages The messages to add in this package. + * + * @return void + * + */ + public function addMessages($messages) + { + $this->messages = array_merge($this->messages, $messages); + } + + /** + * + * Gets the messages for this package. + * + * @return array + * + */ + public function getMessages() + { + return $this->messages; + } + + + /** + * + * Gets the message of the given key for this package. + * + * @param string $key the key of the message to return + * + * @return mixed The message translation string, or false if not found. + * + */ + public function getMessage($key) + { + if (isset($this->messages[$key])) { + return $this->messages[$key]; + } + + return false; + } + + /** + * + * Sets the formatter name for this package. + * + * @param string $formatter The formatter name for this package. + * + * @return void + * + */ + public function setFormatter($formatter) + { + $this->formatter = $formatter; + } + + /** + * + * Gets the formatter name for this package. + * + * @return string + * + */ + public function getFormatter() + { + return $this->formatter; + } + + /** + * + * Sets the fallback package name. + * + * @param string $fallback The fallback package name. + * + * @return void + * + */ + public function setFallback($fallback) + { + $this->fallback = $fallback; + } + + /** + * + * Gets the fallback package name. + * + * @return string + * + */ + public function getFallback() + { + return $this->fallback; + } +} diff --git a/app/vendor/aura/intl/src/PackageFactory.php b/app/vendor/aura/intl/src/PackageFactory.php new file mode 100644 index 000000000..a7a4a27aa --- /dev/null +++ b/app/vendor/aura/intl/src/PackageFactory.php @@ -0,0 +1,47 @@ +setFallback($info['fallback']); + } + if (isset($info['formatter'])) { + $package->setFormatter($info['formatter']); + } + if (isset($info['messages'])) { + $package->setMessages($info['messages']); + } + return $package; + } +} diff --git a/app/vendor/aura/intl/src/PackageLocator.php b/app/vendor/aura/intl/src/PackageLocator.php new file mode 100644 index 000000000..d719c3743 --- /dev/null +++ b/app/vendor/aura/intl/src/PackageLocator.php @@ -0,0 +1,107 @@ + $locales) { + foreach ($locales as $locale => $spec) { + $this->set($name, $locale, $spec); + } + } + } + + /** + * + * Sets a Package object. + * + * @param string $name The package name. + * + * @param string $locale The locale for the package. + * + * @param callable $spec A callable that returns a package. + * + * @return void + * + */ + public function set($name, $locale, callable $spec) + { + $this->registry[$name][$locale] = $spec; + $this->converted[$name][$locale] = false; + } + + /** + * + * Gets a Package object. + * + * @param string $name The package name. + * + * @param string $locale The locale for the package. + * + * @return Package + * + */ + public function get($name, $locale) + { + if (! isset($this->registry[$name][$locale])) { + throw new Exception("Package '$name' with locale '$locale' is not registered."); + } + + if (! $this->converted[$name][$locale]) { + $func = $this->registry[$name][$locale]; + $this->registry[$name][$locale] = $func(); + $this->converted[$name][$locale] = true; + } + + return $this->registry[$name][$locale]; + } +} diff --git a/app/vendor/aura/intl/src/PackageLocatorInterface.php b/app/vendor/aura/intl/src/PackageLocatorInterface.php new file mode 100644 index 000000000..e4b8532fb --- /dev/null +++ b/app/vendor/aura/intl/src/PackageLocatorInterface.php @@ -0,0 +1,49 @@ +locale = $locale; + $this->package = $package; + $this->formatter = $formatter; + $this->fallback = $fallback; + } + + /** + * + * Gets the message translation by its key. + * + * @param string $key The message key. + * + * @return mixed The message translation string, or false if not found. + * + */ + protected function getMessage($key) + { + $message = $this->package->getMessage($key); + if ($message) { + return $message; + } + + if ($this->fallback) { + // get the message from the fallback translator + $message = $this->fallback->getMessage($key); + if ($message) { + // speed optimization: retain locally + $this->package->addMessage($key, $message); + // done! + return $message; + } + } + + // no local message, no fallback + return false; + } + + /** + * + * Translates the message indicated by they key, replacing token values + * along the way. + * + * @param string $key The message key. + * + * @param array $tokens_values Token values to interpolate into the + * message. + * + * @return string The translated message with tokens replaced. + * + */ + public function translate($key, array $tokens_values = []) + { + // get the message string + $message = $this->getMessage($key); + + // do we have a message string? + if (! $message) { + // no, return the message key as-is + $message = $key; + } + + // are there token replacement values? + if (empty($tokens_values)) { + // no, return the message string as-is + return $message; + } + + // run message string through formatter to replace tokens with values + return $this->formatter->format($this->locale, $message, $tokens_values); + } + + /** + * + * An object of type Package + * + * @return Package + * + */ + public function getPackage() + { + return $this->package; + } +} diff --git a/app/vendor/aura/intl/src/TranslatorFactory.php b/app/vendor/aura/intl/src/TranslatorFactory.php new file mode 100644 index 000000000..8cc543888 --- /dev/null +++ b/app/vendor/aura/intl/src/TranslatorFactory.php @@ -0,0 +1,57 @@ +class; + return new $class($locale, $package, $formatter, $fallback); + } +} diff --git a/app/vendor/aura/intl/src/TranslatorInterface.php b/app/vendor/aura/intl/src/TranslatorInterface.php new file mode 100644 index 000000000..8367a4c89 --- /dev/null +++ b/app/vendor/aura/intl/src/TranslatorInterface.php @@ -0,0 +1,32 @@ +packages = $packages; + $this->factory = $factory; + $this->formatters = $formatters; + $this->setLocale($locale); + } + + /** + * + * Sets the default locale code. + * + * @param string $locale The new locale code. + * + * @return void + * + */ + public function setLocale($locale) + { + $this->locale = $locale; + } + + /** + * + * Returns the default locale code. + * + * @return string + * + */ + public function getLocale() + { + return $this->locale; + } + + /** + * + * The TranslatorFactory object + * + * @return TranslatorFactory + */ + public function getFactory() + { + return $this->factory; + } + + /** + * + * An object of type PackageLocator + * + * @return PackageLocator + * + */ + public function getPackages() + { + return $this->packages; + } + + /** + * + * An object of type FormatterLocator + * + * @return FormatterLocator + * + */ + public function getFormatters() + { + return $this->formatters; + } + + /** + * + * Gets a translator from the registry by package for a locale. + * + * @param string $name The translator package to retrieve. + * + * @param string $locale The locale to use; if empty, uses the default + * locale. + * + * @return TranslatorInterface A translator object. + * + */ + public function get($name, $locale = null) + { + if (! $name) { + return null; + } + + if (! $locale) { + $locale = $this->getLocale(); + } + + if (! isset($this->registry[$name][$locale])) { + + // get the package descriptor + $package = $this->packages->get($name, $locale); + + // build a translator; note the recursive nature of the + // 'fallback' param at the very end. + $translator = $this->factory->newInstance( + $locale, + $package, + $this->formatters->get($package->getFormatter()), + $this->get($package->getFallback(), $locale) + ); + + // retain in the registry + $this->registry[$name][$locale] = $translator; + } + + return $this->registry[$name][$locale]; + } +} diff --git a/app/vendor/aura/intl/src/TranslatorLocatorFactory.php b/app/vendor/aura/intl/src/TranslatorLocatorFactory.php new file mode 100644 index 000000000..907e60c38 --- /dev/null +++ b/app/vendor/aura/intl/src/TranslatorLocatorFactory.php @@ -0,0 +1,45 @@ + function () { + return new \Aura\Intl\BasicFormatter; + }, + 'intl' => function () { + return new \Aura\Intl\IntlFormatter; + }, + ]), + new TranslatorFactory, + 'en_US' + ); + } +} diff --git a/app/vendor/aura/intl/tests/BasicFormatterTest.php b/app/vendor/aura/intl/tests/BasicFormatterTest.php new file mode 100644 index 000000000..b70781b56 --- /dev/null +++ b/app/vendor/aura/intl/tests/BasicFormatterTest.php @@ -0,0 +1,29 @@ +newFormatter(); + + $locale = 'en_US'; + $expect = 'Hello world 88!'; + $tokens_values = ['foo' => 'world', 'bar' => '88', 'baz' => '!']; + + $string = 'Hello {foo} {bar}{baz}'; + $actual = $formatter->format($locale, $string, $tokens_values); + $this->assertSame($expect, $actual); + + $tokens_values = ['array' => ['foo', 'bar', 'baz']]; + $string = 'Array {array}'; + $expect = 'Array "foo", "bar", "baz"'; + $actual = $formatter->format($locale, $string, $tokens_values); + $this->assertSame($expect, $actual); + } +} diff --git a/app/vendor/aura/intl/tests/FormatterLocatorTest.php b/app/vendor/aura/intl/tests/FormatterLocatorTest.php new file mode 100644 index 000000000..c1286792c --- /dev/null +++ b/app/vendor/aura/intl/tests/FormatterLocatorTest.php @@ -0,0 +1,37 @@ + function () { + return new \Aura\Intl\MockFormatter; + }, + ]); + + $expect = 'Aura\Intl\MockFormatter'; + $actual = $formatters->get('mock'); + $this->assertInstanceOf($expect, $actual); + } + + public function testSetAndGet() + { + $formatters = new FormatterLocator; + $formatters->set('mock', function () { + return new \Aura\Intl\MockFormatter; + }); + + $expect = 'Aura\Intl\MockFormatter'; + $actual = $formatters->get('mock'); + $this->assertInstanceOf($expect, $actual); + } + + public function testGet_noSuchFormatter() + { + $formatters = new FormatterLocator; + $this->setExpectedException('Aura\Intl\Exception\FormatterNotMapped'); + $formatters->get('noSuchFormatter'); + } +} diff --git a/app/vendor/aura/intl/tests/IntlFormatterTest.php b/app/vendor/aura/intl/tests/IntlFormatterTest.php new file mode 100644 index 000000000..ea8399c74 --- /dev/null +++ b/app/vendor/aura/intl/tests/IntlFormatterTest.php @@ -0,0 +1,195 @@ +markTestSkipped('This test is skipped if the Intl Extension is not loaded.'); + } + } + + public function testIntlVersion() + { + $this->setExpectedException('Aura\Intl\Exception\IcuVersionTooLow'); + $formatter = new IntlFormatter('4.7'); + } + + /** + * This test fails on PHP 5.4.4 + * The return value expected is No pages , but returns false + * + * var_dump( msgfmt_get_error_message($fmt) ); + * U_ZERO_ERROR + * + * It seems $fmt = msgfmt_create($locale, $string); is not creating with + * this string , so the msgfmt_format() throws expects parameter 1 to be + * MessageFormatter, null given error. + */ + public function testFormat_plural() + { + $formatter = $this->newFormatter(); + $locale = 'en_US'; + $string = '{pages,plural,' + . '=0{No pages.}' + . '=1{One page only.}' + . 'other{Page {page} of # pages.}' + . '}'; + + $tokens_values = ['page' => 0, 'pages' => 0]; + $expect = 'No pages.'; + $actual = $formatter->format($locale, $string, $tokens_values); + $this->assertSame($expect, $actual); + + $tokens_values = ['page' => 1, 'pages' => 1]; + $expect = 'One page only.'; + $actual = $formatter->format($locale, $string, $tokens_values); + $this->assertSame($expect, $actual); + + $tokens_values = ['page' => 1, 'pages' => 2]; + $expect = 'Page 1 of 2 pages.'; + $actual = $formatter->format($locale, $string, $tokens_values); + $this->assertSame($expect, $actual); + } + + /** + * @dataProvider provide_testFormat_select + */ + public function testFormat_select($tokens_values, $expect) + { + $locale = 'en_US'; + $string = " + {gender, select, + female { + {count, plural, offset:1 + =0 {{from} does not give a party.} + =1 {{from} invites {to} to her party.} + =2 {{from} invites {to} and one other person to her party.} + other {{from} invites {to} as one of the # people invited to her party.} + } + } + male { + {count, plural, offset:1 + =0 {{from} does not give a party.} + =1 {{from} invites {to} to his party.} + =2 {{from} invites {to} and one other person to his party.} + other {{from} invites {to} as one of the # other people invited to his party.} + } + } + other { + {count, plural, offset:1 + =0 {{from} does not give a party.} + =1 {{from} invites {to} to their party.} + =2 {{from} invites {to} and one other person to their party.} + other {{from} invites {to} as one of the # other people invited to their party.} + } + } + }"; + + $formatter = $this->newFormatter(); + $actual = $formatter->format($locale, $string, $tokens_values); + $this->assertSame(trim($expect), trim($actual)); + } + + public function provide_testFormat_select() + { + return [ + [array('gender' => 'female', 'count' => 0, 'from' => 'Alice', 'to' => 'Bob'), 'Alice does not give a party.'], + [array('gender' => 'male', 'count' => 1, 'from' => 'Bob', 'to' => 'Alice'), 'Bob invites Alice to his party.'], + [array('gender' => 'none', 'count' => 2, 'from' => 'Alice', 'to' => 'Bob'), 'Alice invites Bob and one other person to their party.'], + [array('gender' => 'female', 'count' => 27, 'from' => 'Alice', 'to' => 'Bob'), 'Alice invites Bob as one of the 26 people invited to her party.'], + ]; + } + + public function testFormat_cannotInstantiateFormatter() + { + $locale = 'en_US'; + // uses {count} instead of #, which should fail + $string = " + {gender, select, + female { + {count, plural, offset:1 + =0 {{from} does not give a party.} + =1 {{from} invites {to} to her party.} + =2 {{from} invites {to} and one other person to her party.} + other {{from} invites {to} as one of the {count} people invited to her party.} + } + } + male { + {count, plural, offset:1 + =0 {{from} does not give a party.} + =1 {{from} invites {to} to his party.} + =2 {{from} invites {to} and one other person to his party.} + other {{from} invites {to} as one of the {count} other people invited to his party.} + } + } + other { + {count, plural, offset:1 + =0 {{from} does not give a party.} + =1 {{from} invites {to} to their party.} + =2 {{from} invites {to} and one other person to their party.} + other {{from} invites {to} as one of the {count} other people invited to their party.} + } + } + }"; + $formatter = $this->newFormatter(); + $this->setExpectedException('Aura\Intl\Exception\CannotFormat'); + $actual = $formatter->format($locale, $string, array('gender' => 'female', 'count' => 5, 'from' => 'Alice', 'to' => 'Bob')); + } + + // @todo MAKE IT SO THAT WE CHECK FOR TOKENS IN THE ARRAY + public function testFormat_cannotFormat() + { + $locale = 'en_US'; + $string = 'Hello {foo}'; + $tokens_values = ['bar' => 'baz']; // no 'foo' token + $formatter = $this->newFormatter(); + + $expect = 'Hello {foo}'; + $actual = $formatter->format($locale, $string, $tokens_values); + $this->assertSame($expect, $actual); + } + + /** + * @dataProvider provide_testIssue6 + */ + public function testIssue6($tokens_values, $expect) + { + $locale = 'en_US'; + $string = '{gender, select, female{{name} is {gender} and she report bugs!} male{{name} is {gender} and he report bugs!} other{{name} report bugs!}}'; + $formatter = $this->newFormatter(); + $actual = $formatter->format($locale, $string, $tokens_values); + $this->assertSame($expect, $actual); + } + + public function testEmptyStringThrowsException() + { + $locale = 'en_US'; + $string = ''; + $formatter = $this->newFormatter(); + if (! defined('HHVM_VERSION')) { + $this->setExpectedException('Aura\Intl\Exception\CannotInstantiateFormatter'); + } + $actual = $formatter->format($locale, $string, []); + if (defined('HHVM_VERSION')) { + // HHVM will instantiate Formatter even if empty string is passed. + $expect = ''; + $this->assertSame($expect, $actual); + } + } + + public function provide_testIssue6() + { + return [ + [array('gender' => 'female', 'name' => 'Alice'), 'Alice is female and she report bugs!'], + [array('gender' => 'male', 'name' => 'Alexander' ), 'Alexander is male and he report bugs!'], + [array('gender' => '', 'name' => 'Unknown'), 'Unknown report bugs!'], + ]; + } +} diff --git a/app/vendor/aura/intl/tests/MockFormatter.php b/app/vendor/aura/intl/tests/MockFormatter.php new file mode 100644 index 000000000..8905b1dca --- /dev/null +++ b/app/vendor/aura/intl/tests/MockFormatter.php @@ -0,0 +1,10 @@ +packages = new PackageLocator([ + 'Vendor.Foo' => [ + 'en_US' => function () { + return new \Aura\Intl\Package; + }, + ], + ]); + } + + public function testGet() + { + // get once to create it the first time + $first = $this->packages->get('Vendor.Foo', 'en_US'); + $this->assertInstanceOf('Aura\Intl\Package', $first); + + // get again to make sure it's the same object + $again = $this->packages->get('Vendor.Foo', 'en_US'); + $this->assertSame($first, $again); + + // try for an unregistered package + $this->setExpectedException('Aura\Intl\Exception'); + $this->packages->get('Vendor.Bar', 'en_US'); + } +} diff --git a/app/vendor/aura/intl/tests/PackageTest.php b/app/vendor/aura/intl/tests/PackageTest.php new file mode 100644 index 000000000..9ca034c79 --- /dev/null +++ b/app/vendor/aura/intl/tests/PackageTest.php @@ -0,0 +1,42 @@ +package = $factory->newInstance([ + 'fallback' => 'Vendor.Fallback', + 'formatter' => 'intl', + 'messages' => [ + 'ERR_NO_SUCH_OPTION' => "The option {option} is not recognized.", + ], + ]); + } + + public function testGet() + { + $expect = 'Vendor.Fallback'; + $actual = $this->package->getFallback(); + $this->assertSame($expect, $actual); + + $expect = 'intl'; + $actual = $this->package->getFormatter(); + $this->assertSame($expect, $actual); + + $expect = [ + 'ERR_NO_SUCH_OPTION' => "The option {option} is not recognized.", + ]; + $actual = $this->package->getMessages(); + $this->assertSame($expect, $actual); + + } +} diff --git a/app/vendor/aura/intl/tests/TranslatorLocatorFactoryTest.php b/app/vendor/aura/intl/tests/TranslatorLocatorFactoryTest.php new file mode 100644 index 000000000..5d7f04c0f --- /dev/null +++ b/app/vendor/aura/intl/tests/TranslatorLocatorFactoryTest.php @@ -0,0 +1,11 @@ +assertInstanceOf('Aura\Intl\TranslatorLocator', $factory->newInstance()); + } +} diff --git a/app/vendor/aura/intl/tests/TranslatorLocatorTest.php b/app/vendor/aura/intl/tests/TranslatorLocatorTest.php new file mode 100644 index 000000000..7c6ed130f --- /dev/null +++ b/app/vendor/aura/intl/tests/TranslatorLocatorTest.php @@ -0,0 +1,113 @@ + "The option {option} is not recognized.", + ] + ); + }; + + $registry['Vendor.Package']['pt_BR'] = function () { + return new \Aura\Intl\Package( + 'mock', + null, + [ + 'ERR_NO_SUCH_OPTION' => "O {option} opção não é reconhecido.", + ] + ); + }; + + $this->packages = new PackageLocator($registry); + + $this->formatters = new FormatterLocator([ + 'mock' => function () { + return new MockFormatter; + }, + ]); + + $this->factory = new TranslatorFactory; + + $this->translators = new TranslatorLocator( + $this->packages, + $this->formatters, + $this->factory, + 'en_US' + ); + } + + public function testSetAndGetLocale() + { + $expect = 'pt_BR'; + $this->translators->setLocale($expect); + $actual = $this->translators->getLocale(); + $this->assertSame($expect, $actual); + } + + public function testGetFactory() + { + $actual = $this->translators->getFactory(); + $this->assertSame($this->factory, $actual); + } + + public function testGet() + { + $actual = $this->translators->get('Vendor.Package'); + $this->assertInstanceOf('Aura\Intl\Translator', $actual); + } + + public function testGetPackages() + { + $actual = $this->translators->getPackages(); + $this->assertSame($this->packages, $actual); + } + + public function testGetFormatterLocator() + { + $actual = $this->translators->getFormatters(); + $this->assertSame($this->formatters, $actual); + } + + public function testIssue9() + { + $this->packages->set('Vendor.Package', 'en_UK', function () { + $package = new Package('mock'); + $package->setMessages([ + 'FOO' => 'The text for "foo."', + 'BAR' => 'The text for "bar."', + ]); + return $package; + }); + + $translator = $this->translators->get('Vendor.Package', 'en_UK'); + $expect = 'The text for "foo."'; + $this->assertSame($translator->translate('FOO'), $expect); + } + + public function testIssue9Failure() + { + $package = new Package; + $package->setMessages([ + 'FOO' => 'The text for "foo."', + 'BAR' => 'The text for "bar."', + ]); + // $this->packages->set('Vendor.Package', 'en_UK', $package); + // $this->setExpectedException('Exception'); + // $translator = $this->translators->get('Vendor.Package', 'en_UK'); + } +} diff --git a/app/vendor/aura/intl/tests/TranslatorTest.php b/app/vendor/aura/intl/tests/TranslatorTest.php new file mode 100644 index 000000000..e7e8c58d7 --- /dev/null +++ b/app/vendor/aura/intl/tests/TranslatorTest.php @@ -0,0 +1,101 @@ + 'Foo text', + 'TEXT_BAR' => 'Bar text', + ]; + + protected $formatter; + + protected function newTranslator(TranslatorInterface $fallback = null) + { + return $this->factory->newInstance( + 'en_US', + $this->package, + $this->formatter, + $fallback + ); + } + + protected function setUp() + { + $this->factory = new TranslatorFactory; + $this->formatter = new MockFormatter; + $this->package = new Package; + $this->package->setMessages($this->messages); + $this->translator = $this->newTranslator(); + } + + public function testTranslate() + { + // key exists + $expect = 'Foo text'; + $actual = $this->translator->translate('TEXT_FOO'); + $this->assertSame($expect, $actual); + + // key exists, with tokens passed + $expect = 'Foo text'; + $actual = $this->translator->translate('TEXT_FOO', ['foo' => 'bar']); + $this->assertSame($expect, $actual); + + // key does not exist + $expect = 'TEXT_NONE'; + $actual = $this->translator->translate('TEXT_NONE'); + $this->assertSame($expect, $actual); + } + + public function testTranslate_fallback() + { + $package = new Package; + $package->setMessages([ + 'TEXT_NONE' => 'Fallback text', + ]); + // create fallback translator + $fallback = new Translator( + 'en_US', + $package, + $this->formatter + ); + + // create primary translator with fallback + $translator = $this->newTranslator($fallback); + + // key does not exist in primary, but exists in fallback + $expect = 'Fallback text'; + $actual = $translator->translate('TEXT_NONE'); + $this->assertSame($expect, $actual); + } + + public function testTranslateMissingKey() + { + // using getMockBuilder so we can use with phpunit 4.8 or 5+ without warnings + $formatter = $this->getMockBuilder(get_class($this->formatter)) + ->disableOriginalConstructor() + ->disableOriginalClone() + ->disableArgumentCloning() + ->getMock(); + // create fallback translator + $translator = new Translator('en_US', new Package, $formatter); + + $formatter->expects($this->once()) + ->method('format') + ->with('en_US', 'TEXT', ['var' => 'SOME']) + ->will($this->returnValue('FORMATTED')); + + // key does not exist, with tokens passed + $expect = 'FORMATTED'; + $actual = $translator->translate('TEXT', ['var' => 'SOME']); + $this->assertEquals($expect, $actual); + } +} diff --git a/app/vendor/autoload.php b/app/vendor/autoload.php new file mode 100644 index 000000000..2e073de26 --- /dev/null +++ b/app/vendor/autoload.php @@ -0,0 +1,7 @@ + [ + 'Bake' => $baseDir . '/vendor/cakephp/bake/', + 'DebugKit' => $baseDir . '/vendor/cakephp/debug_kit/', + 'Migrations' => $baseDir . '/vendor/cakephp/migrations/', + 'WyriHaximus/TwigView' => $baseDir . '/vendor/wyrihaximus/twig-view/' + ] +]; \ No newline at end of file diff --git a/app/vendor/cakephp/bake/LICENSE.txt b/app/vendor/cakephp/bake/LICENSE.txt new file mode 100644 index 000000000..5849d31d4 --- /dev/null +++ b/app/vendor/cakephp/bake/LICENSE.txt @@ -0,0 +1,28 @@ +The MIT License + +CakePHP(tm) : The Rapid Development PHP Framework (http://cakephp.org) +Copyright (c) 2005-2018, Cake Software Foundation, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +Cake Software Foundation, Inc. +1785 E. Sahara Avenue, +Suite 490-204 +Las Vegas, Nevada 89104, +United States of America. diff --git a/app/vendor/cakephp/bake/README.md b/app/vendor/cakephp/bake/README.md new file mode 100644 index 000000000..7c6344e5e --- /dev/null +++ b/app/vendor/cakephp/bake/README.md @@ -0,0 +1,38 @@ +# Bake plugin for CakePHP + +[![Build Status](https://img.shields.io/travis/cakephp/bake/master.svg?style=flat-square)](https://travis-ci.org/cakephp/bake) +[![Coverage Status](https://img.shields.io/codecov/c/github/cakephp/bake.svg?style=flat-square)](https://codecov.io/github/cakephp/bake) +[![License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.txt) + +This project provides the code generation functionality for CakePHP. Through a +suite of CLI tools, you can quickly and easily generate code for your application. + +## Installation + +You can install this plugin into your CakePHP application using [composer](http://getcomposer.org). + +The recommended way to install composer packages is: + +``` +composer require --dev cakephp/bake +``` + +## Documentation + +You can find the documentation for bake [on the cookbook](http://book.cakephp.org/3.0/en/bake.html). + +## Testing + +After installing dependencies with composer you can run tests with `phpunit`: + +```bash +vendor/bin/phpunit +``` + +If your changes require changing the templates that bake uses, you can save time updating tests, by +enabling bake's 'overwrite fixture feature'. This will let you re-generate the expected output files +without having to manually edit each one: + +```bash +UPDATE_TEST_COMPARISON_FILES=1 vendor/bin/phpunit +``` diff --git a/app/vendor/cakephp/bake/composer.json b/app/vendor/cakephp/bake/composer.json new file mode 100644 index 000000000..f6143c6e7 --- /dev/null +++ b/app/vendor/cakephp/bake/composer.json @@ -0,0 +1,44 @@ +{ + "name": "cakephp/bake", + "description": "Bake plugin for CakePHP 3", + "type": "cakephp-plugin", + "keywords": ["cakephp", "bake"], + "homepage": "https://github.com/cakephp/bake", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/bake/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/bake/issues", + "forum": "http://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/bake" + }, + "require": { + "php": ">=5.6.0", + "cakephp/cakephp": "^3.5.10", + "cakephp/plugin-installer": "^1.0", + "wyrihaximus/twig-view": "^4.2.1" + }, + "require-dev": { + "cakephp/cakephp-codesniffer": "^3.0", + "phpunit/phpunit": "^5.7 | ^6.0" + }, + "autoload": { + "psr-4": { + "Bake\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "BakeTest\\": "tests/test_app/Plugin/BakeTest/src", + "Pastry\\PastryTest\\": "tests/test_app/Plugin/PastryTest/src", + "Bake\\Test\\": "tests", + "Bake\\Test\\App\\": "tests/test_app/App", + "Cake\\Test\\": "./vendor/cakephp/cakephp/tests" + } + } +} diff --git a/app/vendor/cakephp/bake/phpstan.neon b/app/vendor/cakephp/bake/phpstan.neon new file mode 100644 index 000000000..d08453c72 --- /dev/null +++ b/app/vendor/cakephp/bake/phpstan.neon @@ -0,0 +1,8 @@ +parameters: + autoload_files: + - tests/bootstrap.php + ignoreErrors: + - '#Call to an undefined method Cake\\Datasource\\SchemaInterface::columnType\(\)#' + - '#Call to an undefined method Cake\\Database\\Type::getDateTimeClassName\(\)#' + - '#Constant TESTS not found#' + - '#Call to an undefined method Cake\\ORM\\Association::junction\(\)#' diff --git a/app/vendor/cakephp/bake/src/Shell/BakeShell.php b/app/vendor/cakephp/bake/src/Shell/BakeShell.php new file mode 100644 index 000000000..d5c1e2439 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/BakeShell.php @@ -0,0 +1,322 @@ +connection to the active task if a connection param is set. + * + * @return void + */ + public function startup() + { + parent::startup(); + Configure::write('debug', true); + Cache::disable(); + if (!Plugin::loaded('WyriHaximus/TwigView')) { + Plugin::load('WyriHaximus/TwigView', ['bootstrap' => true]); + } + + $task = $this->_camelize($this->command); + + if (isset($this->{$task}) && !in_array($task, ['Project'])) { + if (isset($this->params['connection'])) { + $this->{$task}->connection = $this->params['connection']; + } + if (isset($this->params['tablePrefix'])) { + $this->{$task}->tablePrefix = $this->params['tablePrefix']; + } + } + if (isset($this->params['connection'])) { + $this->connection = $this->params['connection']; + } + + if ($this->params['quiet']) { + $this->interactive = false; + if (isset($this->{$task}) && !in_array($task, ['Project'])) { + $this->{$task}->interactive = false; + } + } + } + + /** + * Override main() to handle action + * + * @return bool + */ + public function main() + { + if ($this->args && $this->args[0] === 'view') { + $this->out('The view command has been renamed.'); + $this->out('To create template files, please use the template command:', 2); + $args = $this->args; + array_shift($args); + $args = implode($args, ' '); + $this->out(sprintf(' `bin/cake bake template %s`', $args), 2); + + return false; + } + + $connections = ConnectionManager::configured(); + if (empty($connections)) { + $this->out('Your database configuration was not found.'); + $this->out('Add your database connection information to config/app.php.'); + + return false; + } + $this->out('The following commands can be used to generate skeleton code for your application.', 2); + $this->out('Available bake commands:', 2); + $this->out('- all'); + $names = []; + foreach ($this->tasks as $task) { + list(, $name) = pluginSplit($task); + $names[] = Inflector::underscore($name); + } + sort($names); + foreach ($names as $name) { + $this->out('- ' . $name); + } + $this->out(''); + $this->out('By using `cake bake [name]` you can invoke a specific bake task.'); + + return false; + } + + /** + * Locate the tasks bake will use. + * + * Scans the following paths for tasks that are subclasses of + * Cake\Shell\Task\BakeTask: + * + * - Cake/Shell/Task/ + * - Shell/Task for each loaded plugin + * - App/Shell/Task/ + * + * @return void + */ + public function loadTasks() + { + $tasks = []; + + foreach (Plugin::loaded() as $plugin) { + $tasks = $this->_findTasks( + $tasks, + Plugin::classPath($plugin), + str_replace('/', '\\', $plugin), + $plugin + ); + } + $tasks = $this->_findTasks($tasks, APP, Configure::read('App.namespace')); + + $this->tasks = array_values($tasks); + parent::loadTasks(); + } + + /** + * Append matching tasks in $path to the $tasks array. + * + * @param array $tasks The task list to modify and return. + * @param string $path The base path to look in. + * @param string $namespace The base namespace. + * @param string|null $prefix The prefix to append. + * @return array Updated tasks. + */ + protected function _findTasks($tasks, $path, $namespace, $prefix = null) + { + $path .= 'Shell/Task'; + if (!is_dir($path)) { + return $tasks; + } + $candidates = $this->_findClassFiles($path, $namespace); + $classes = $this->_findTaskClasses($candidates); + foreach ($classes as $class) { + list(, $name) = namespaceSplit($class); + $name = substr($name, 0, -4); + $fullName = ($prefix ? $prefix . '.' : '') . $name; + $tasks[$name] = $fullName; + } + + return $tasks; + } + + /** + * Find task classes in a given path. + * + * @param string $path The path to scan. + * @param string $namespace Namespace. + * @return array An array of files that may contain bake tasks. + */ + protected function _findClassFiles($path, $namespace) + { + $iterator = new \DirectoryIterator($path); + $candidates = []; + foreach ($iterator as $item) { + if ($item->isDot() || $item->isDir()) { + continue; + } + $name = $item->getBasename('.php'); + $candidates[] = $namespace . '\Shell\Task\\' . $name; + } + + return $candidates; + } + + /** + * Find bake tasks in a given set of files. + * + * @param array $files The array of files. + * @return array An array of matching classes. + */ + protected function _findTaskClasses($files) + { + $classes = []; + foreach ($files as $className) { + if (!class_exists($className)) { + continue; + } + $reflect = new \ReflectionClass($className); + if (!$reflect->isInstantiable()) { + continue; + } + if (!$reflect->isSubclassOf('Bake\Shell\Task\BakeTask')) { + continue; + } + $classes[] = $className; + } + + return $classes; + } + + /** + * Quickly bake the MVC + * + * @param string|null $name Name. + * @return bool + */ + public function all($name = null) + { + $this->out('Bake All'); + $this->hr(); + + if (!empty($this->params['connection'])) { + $this->connection = $this->params['connection']; + } + + if (empty($name) && !$this->param('everything')) { + $this->Model->connection = $this->connection; + $this->out('Possible model names based on your database:'); + foreach ($this->Model->listUnskipped() as $table) { + $this->out('- ' . $table); + } + $this->out('Run `cake bake all [name]` to generate skeleton files.'); + + return false; + } + + $allTables = collection([$name]); + $filteredTables = $allTables; + + if ($this->param('everything')) { + $this->Model->connection = $this->connection; + $filteredTables = collection($this->Model->listUnskipped()); + } + + foreach (['Model', 'Controller', 'Template'] as $task) { + $filteredTables->each(function ($tableName) use ($task) { + $tableName = $this->_camelize($tableName); + $this->{$task}->connection = $this->connection; + $this->{$task}->interactive = $this->interactive; + $this->{$task}->main($tableName); + }); + } + + $this->out('Bake All complete.', 1, Shell::QUIET); + + return true; + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->setDescription( + 'The Bake script generates controllers, models and template files for your application.' . + ' If run with no command line arguments, Bake guides the user through the class creation process.' . + ' You can customize the generation process by telling Bake where different parts of your application' . + ' are using command line arguments.' + )->addSubcommand('all', [ + 'help' => 'Bake a complete MVC skeleton.', + ])->addOption('everything', [ + 'help' => 'Bake a complete MVC skeleton, using all the available tables. ' . + 'Usage: "bake all --everything"', + 'default' => false, + 'boolean' => true, + ])->addOption('prefix', [ + 'help' => 'Prefix to bake controllers and templates into.' + ])->addOption('tablePrefix', [ + 'help' => 'Table prefix to be used in models.', + 'default' => null + ]); + + $parser = $this->_setCommonOptions($parser); + + foreach ($this->_taskMap as $task => $config) { + $taskParser = $this->{$task}->getOptionParser(); + $this->{$task}->interactive = $this->interactive; + $parser->addSubcommand(Inflector::underscore($task), [ + 'help' => $taskParser->getDescription(), + 'parser' => $taskParser + ]); + } + + return $parser; + } +} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/BakeTask.php b/app/vendor/cakephp/bake/src/Shell/Task/BakeTask.php new file mode 100644 index 000000000..57bfacdd4 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/Task/BakeTask.php @@ -0,0 +1,232 @@ +connection) && !empty($this->params['connection'])) { + $this->connection = $this->params['connection']; + } + } + + /** + * Get the prefix name. + * + * Handles camelcasing each namespace in the prefix path. + * + * @return string The inflected prefix path. + */ + protected function _getPrefix() + { + $prefix = $this->param('prefix'); + if (!$prefix) { + return ''; + } + $parts = explode('/', $prefix); + + return implode('/', array_map([$this, '_camelize'], $parts)); + } + + /** + * Gets the path for output. Checks the plugin property + * and returns the correct path. + * + * @return string Path to output. + */ + public function getPath() + { + $path = APP . $this->pathFragment; + if (isset($this->plugin)) { + $path = $this->_pluginPath($this->plugin) . 'src/' . $this->pathFragment; + } + $prefix = $this->_getPrefix(); + if ($prefix) { + $path .= $prefix . DS; + } + + return str_replace('/', DS, $path); + } + + /** + * Base execute method parses some parameters and sets some properties on the bake tasks. + * call when overriding execute() + * + * @return void + */ + public function main() + { + if (isset($this->params['plugin'])) { + $parts = explode('/', $this->params['plugin']); + $this->plugin = implode('/', array_map([$this, '_camelize'], $parts)); + if (strpos($this->plugin, '\\')) { + $this->abort('Invalid plugin namespace separator, please use / instead of \ for plugins.'); + + return; + } + } + if (isset($this->params['connection'])) { + $this->connection = $this->params['connection']; + } + } + + /** + * Executes an external shell command and pipes its output to the stdout + * + * @param string $command the command to execute + * @return void + * @throws \RuntimeException if any errors occurred during the execution + */ + public function callProcess($command) + { + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'] + ]; + $this->_io->verbose('Running ' . $command); + $process = proc_open( + $command, + $descriptorSpec, + $pipes + ); + if (!is_resource($process)) { + $this->abort('Could not start subprocess.'); + + return; + } + fclose($pipes[0]); + + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + $error = stream_get_contents($pipes[2]); + fclose($pipes[2]); + $exit = proc_close($process); + + if ($exit !== 0) { + throw new \RuntimeException($error); + } + + $this->out($output); + } + + /** + * Handles splitting up the plugin prefix and classname. + * + * Sets the plugin parameter and plugin property. + * + * @param string $name The name to possibly split. + * @return string The name without the plugin prefix. + */ + protected function _getName($name) + { + if (strpos($name, '.')) { + list($plugin, $name) = pluginSplit($name); + $this->plugin = $this->params['plugin'] = $plugin; + } + + return $name; + } + + /** + * Delete empty file in a given path + * + * @param string $path Path to folder which contains 'empty' file. + * @return void + */ + protected function _deleteEmptyFile($path) + { + $File = new File($path); + if ($File->exists()) { + $File->delete(); + $this->out(sprintf('Deleted `%s`', $path), 1, Shell::QUIET); + } + } + + /** + * Get the option parser for this task. + * + * This base class method sets up some commonly used options. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + return $this->_setCommonOptions(parent::getOptionParser()); + } +} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/BakeTemplateTask.php b/app/vendor/cakephp/bake/src/Shell/Task/BakeTemplateTask.php new file mode 100644 index 000000000..414064278 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/Task/BakeTemplateTask.php @@ -0,0 +1,96 @@ +View) { + return $this->View; + } + + $theme = isset($this->params['theme']) ? $this->params['theme'] : ''; + + $viewOptions = [ + 'helpers' => [ + 'Bake.Bake', + 'Bake.DocBlock' + ], + 'theme' => $theme + ]; + + $view = new BakeView(new Request(), new Response(), null, $viewOptions); + $event = new Event('Bake.initialize', $view); + EventManager::instance()->dispatch($event); + $this->View = $event->subject; + + return $this->View; + } + + /** + * Runs the template + * + * @param string $template bake template to render + * @param array|null $vars Additional vars to set to template scope. + * @return string contents of generated code template + */ + public function generate($template, $vars = null) + { + if ($vars !== null) { + $this->set($vars); + } + + $this->getView()->set($this->viewVars); + + try { + return $this->View->render($template); + } catch (MissingTemplateException $e) { + $this->_io->verbose(sprintf('No bake template found for "%s"', $template)); + + return ''; + } + } +} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/BehaviorTask.php b/app/vendor/cakephp/bake/src/Shell/Task/BehaviorTask.php new file mode 100644 index 000000000..2657e687a --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/Task/BehaviorTask.php @@ -0,0 +1,55 @@ +_getPrefix(); + if ($prefix) { + $prefix = '\\' . str_replace('/', '\\', $prefix); + } + + $namespace = Configure::read('App.namespace'); + if ($this->plugin) { + $namespace = $this->_pluginNamespace($this->plugin); + } + + return compact('namespace', 'prefix'); + } + + /** + * Bake the Cell class and template file. + * + * @param string $name The name of the cell to make. + * @return string + */ + public function bake($name) + { + $this->bakeTemplate($name); + + return parent::bake($name); + } + + /** + * Bake an empty file for a cell. + * + * @param string $name The name of the cell a template is needed for. + * @return void + */ + public function bakeTemplate($name) + { + $restore = $this->pathFragment; + + $this->pathFragment = 'Template/Cell/'; + $path = $this->getPath(); + $path .= implode(DS, [$name, 'display.ctp']); + + $this->pathFragment = $restore; + + $this->createFile($path, ''); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + $parser + ->addOption('prefix', [ + 'help' => 'The namespace prefix to use.' + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/ComponentTask.php b/app/vendor/cakephp/bake/src/Shell/Task/ComponentTask.php new file mode 100644 index 000000000..aee4670d1 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/Task/ComponentTask.php @@ -0,0 +1,55 @@ +_getName($name); + + if (empty($name)) { + $this->out('Possible controllers based on your current database:'); + foreach ($this->listAll() as $table) { + $this->out('- ' . $this->_camelize($table)); + } + + return true; + } + + $controller = $this->_camelize($name); + $this->bake($controller); + } + + /** + * Bake All the controllers at once. Will only bake controllers for models that exist. + * + * @return void + */ + public function all() + { + $tables = $this->listAll(); + foreach ($tables as $table) { + TableRegistry::clear(); + $this->main($table); + } + } + + /** + * Assembles and writes a Controller file + * + * @param string $controllerName Controller name already pluralized and correctly cased. + * @return string Baked controller + */ + public function bake($controllerName) + { + $this->out("\n" . sprintf('Baking controller class for %s...', $controllerName), 1, Shell::QUIET); + + $actions = []; + if (!$this->param('no-actions') && !$this->param('actions')) { + $actions = ['index', 'view', 'add', 'edit', 'delete']; + } + if ($this->param('actions')) { + $actions = array_map('trim', explode(',', $this->param('actions'))); + $actions = array_filter($actions); + } + + $helpers = $this->getHelpers(); + $components = $this->getComponents(); + + $prefix = $this->_getPrefix(); + if ($prefix) { + $prefix = '\\' . str_replace('/', '\\', $prefix); + } + + $namespace = Configure::read('App.namespace'); + if ($this->plugin) { + $namespace = $this->_pluginNamespace($this->plugin); + } + + $currentModelName = $controllerName; + $plugin = $this->plugin; + if ($plugin) { + $plugin .= '.'; + } + + if (TableRegistry::exists($plugin . $currentModelName)) { + $modelObj = TableRegistry::get($plugin . $currentModelName); + } else { + $modelObj = TableRegistry::get($plugin . $currentModelName, [ + 'connectionName' => $this->connection + ]); + } + + $pluralName = $this->_variableName($currentModelName); + $singularName = $this->_singularName($currentModelName); + $singularHumanName = $this->_singularHumanName($controllerName); + $pluralHumanName = $this->_variableName($controllerName); + + $defaultModel = sprintf('%s\Model\Table\%sTable', $namespace, $controllerName); + if (!class_exists($defaultModel)) { + $defaultModel = null; + } + $entityClassName = $this->_entityName($modelObj->getAlias()); + + $data = compact( + 'actions', + 'admin', + 'components', + 'currentModelName', + 'defaultModel', + 'entityClassName', + 'helpers', + 'modelObj', + 'namespace', + 'plugin', + 'pluralHumanName', + 'pluralName', + 'prefix', + 'singularHumanName', + 'singularName' + ); + $data['name'] = $controllerName; + + $out = $this->bakeController($controllerName, $data); + $this->bakeTest($controllerName); + + return $out; + } + + /** + * Generate the controller code + * + * @param string $controllerName The name of the controller. + * @param array $data The data to turn into code. + * @return string The generated controller file. + */ + public function bakeController($controllerName, array $data) + { + $data += [ + 'name' => null, + 'namespace' => null, + 'prefix' => null, + 'actions' => null, + 'helpers' => null, + 'components' => null, + 'plugin' => null, + 'pluginPath' => null, + ]; + + $this->BakeTemplate->set($data); + + $contents = $this->BakeTemplate->generate('Controller/controller'); + + $path = $this->getPath(); + $filename = $path . $controllerName . 'Controller.php'; + $this->createFile($filename, $contents); + + return $contents; + } + + /** + * Assembles and writes a unit test file + * + * @param string $className Controller class name + * @return string|null Baked test + */ + public function bakeTest($className) + { + if (!empty($this->params['no-test'])) { + return null; + } + $this->Test->plugin = $this->plugin; + $this->Test->connection = $this->connection; + $this->Test->interactive = $this->interactive; + + return $this->Test->bake('Controller', $className); + } + + /** + * Get the list of components for the controller. + * + * @return array + */ + public function getComponents() + { + $components = []; + if (!empty($this->params['components'])) { + $components = explode(',', $this->params['components']); + $components = array_values(array_filter(array_map('trim', $components))); + } + + return $components; + } + + /** + * Get the list of helpers for the controller. + * + * @return array + */ + public function getHelpers() + { + $helpers = []; + if (!empty($this->params['helpers'])) { + $helpers = explode(',', $this->params['helpers']); + $helpers = array_values(array_filter(array_map('trim', $helpers))); + } + + return $helpers; + } + + /** + * Outputs and gets the list of possible controllers from database + * + * @return array Set of controllers + */ + public function listAll() + { + $this->Model->connection = $this->connection; + + return $this->Model->listUnskipped(); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + $parser->setDescription( + 'Bake a controller skeleton.' + )->addArgument('name', [ + 'help' => 'Name of the controller to bake (without the `Controller` suffix). ' . + 'You can use Plugin.name to bake controllers into plugins.' + ])->addOption('components', [ + 'help' => 'The comma separated list of components to use.' + ])->addOption('helpers', [ + 'help' => 'The comma separated list of helpers to use.' + ])->addOption('prefix', [ + 'help' => 'The namespace/routing prefix to use.' + ])->addOption('actions', [ + 'help' => 'The comma separated list of actions to generate. ' . + 'You can include custom methods provided by your template set here.' + ])->addOption('no-test', [ + 'boolean' => true, + 'help' => 'Do not generate a test skeleton.' + ])->addOption('no-actions', [ + 'boolean' => true, + 'help' => 'Do not generate basic CRUD action methods.' + ])->addSubcommand('all', [ + 'help' => 'Bake all controllers with CRUD methods.' + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/FixtureTask.php b/app/vendor/cakephp/bake/src/Shell/Task/FixtureTask.php new file mode 100644 index 000000000..421a49bfe --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/Task/FixtureTask.php @@ -0,0 +1,449 @@ +plugin)) { + $path = $this->_pluginPath($this->plugin) . 'tests/' . $dir; + } + + return str_replace('/', DS, $path); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser = $parser->setDescription( + 'Generate fixtures for use with the test suite. You can use `bake fixture all` to bake all fixtures.' + )->addArgument('name', [ + 'help' => 'Name of the fixture to bake (without the `Fixture` suffix). ' . + 'You can use Plugin.name to bake plugin fixtures.' + ])->addOption('table', [ + 'help' => 'The table name if it does not follow conventions.', + ])->addOption('count', [ + 'help' => 'When using generated data, the number of records to include in the fixture(s).', + 'short' => 'n', + 'default' => 1 + ])->addOption('schema', [ + 'help' => 'Create a fixture that imports schema, instead of dumping a schema snapshot into the fixture.', + 'short' => 's', + 'boolean' => true + ])->addOption('records', [ + 'help' => 'Generate a fixture with records from the non-test database.' . + ' Used with --count and --conditions to limit which records are added to the fixture.', + 'short' => 'r', + 'boolean' => true + ])->addOption('conditions', [ + 'help' => 'The SQL snippet to use when importing records.', + 'default' => '1=1', + ])->addSubcommand('all', [ + 'help' => 'Bake all fixture files for tables in the chosen connection.' + ]); + + return $parser; + } + + /** + * Execution method always used for tasks + * Handles dispatching to interactive, named, or all processes. + * + * @param string|null $name The name of the fixture to bake. + * @return null|bool + */ + public function main($name = null) + { + parent::main(); + $name = $this->_getName($name); + + if (empty($name)) { + $this->out('Choose a fixture to bake from the following:'); + foreach ($this->Model->listUnskipped() as $table) { + $this->out('- ' . $this->_camelize($table)); + } + + return true; + } + + $table = null; + if (isset($this->params['table'])) { + $table = $this->params['table']; + } + $model = $this->_camelize($name); + $this->bake($model, $table); + } + + /** + * Bake All the Fixtures at once. Will only bake fixtures for models that exist. + * + * @return void + */ + public function all() + { + $tables = $this->Model->listUnskipped(); + + foreach ($tables as $table) { + $this->main($table); + } + } + + /** + * Assembles and writes a Fixture file + * + * @param string $model Name of model to bake. + * @param string|null $useTable Name of table to use. + * @return string Baked fixture content + * @throws \RuntimeException + */ + public function bake($model, $useTable = null) + { + $table = $schema = $records = $import = $modelImport = null; + + if (!$useTable) { + $useTable = Inflector::tableize($model); + } elseif ($useTable !== Inflector::tableize($model)) { + $table = $useTable; + } + + $importBits = []; + if (!empty($this->params['schema'])) { + $modelImport = true; + $importBits[] = "'table' => '{$useTable}'"; + } + if (!empty($importBits) && $this->connection !== 'default') { + $importBits[] = "'connection' => '{$this->connection}'"; + } + if (!empty($importBits)) { + $import = sprintf("[%s]", implode(', ', $importBits)); + } + + $connection = ConnectionManager::get($this->connection); + if (!method_exists($connection, 'schemaCollection')) { + throw new \RuntimeException( + 'Cannot generate fixtures for connections that do not implement schemaCollection()' + ); + } + $schemaCollection = $connection->schemaCollection(); + try { + $data = $schemaCollection->describe($useTable); + } catch (Exception $e) { + $useTable = Inflector::underscore($model); + $table = $useTable; + $data = $schemaCollection->describe($useTable); + } + + if ($modelImport === null) { + $schema = $this->_generateSchema($data); + } + + if (empty($this->params['records'])) { + $recordCount = 1; + if (isset($this->params['count'])) { + $recordCount = $this->params['count']; + } + $records = $this->_makeRecordString($this->_generateRecords($data, $recordCount)); + } + if (!empty($this->params['records'])) { + $records = $this->_makeRecordString($this->_getRecordsFromTable($model, $useTable)); + } + + return $this->generateFixtureFile($model, compact('records', 'table', 'schema', 'import')); + } + + /** + * Generate the fixture file, and write to disk + * + * @param string $model name of the model being generated + * @param array $otherVars Contents of the fixture file. + * @return string Content saved into fixture file. + */ + public function generateFixtureFile($model, array $otherVars) + { + $defaults = [ + 'name' => $model, + 'table' => null, + 'schema' => null, + 'records' => null, + 'import' => null, + 'fields' => null, + 'namespace' => Configure::read('App.namespace') + ]; + if ($this->plugin) { + $defaults['namespace'] = $this->_pluginNamespace($this->plugin); + } + $vars = $otherVars + $defaults; + + $path = $this->getPath(); + $filename = $vars['name'] . 'Fixture.php'; + + $this->BakeTemplate->set('model', $model); + $this->BakeTemplate->set($vars); + $content = $this->BakeTemplate->generate('tests/fixture'); + + $this->out("\n" . sprintf('Baking test fixture for %s...', $model), 1, Shell::QUIET); + $this->createFile($path . $filename, $content); + $emptyFile = $path . 'empty'; + $this->_deleteEmptyFile($emptyFile); + + return $content; + } + + /** + * Generates a string representation of a schema. + * + * @param \Cake\Database\Schema\Table $table Table schema + * @return string fields definitions + */ + protected function _generateSchema(Table $table) + { + $cols = $indexes = $constraints = []; + foreach ($table->columns() as $field) { + $fieldData = $table->column($field); + $properties = implode(', ', $this->_values($fieldData)); + $cols[] = " '$field' => [$properties],"; + } + foreach ($table->indexes() as $index) { + $fieldData = $table->index($index); + $properties = implode(', ', $this->_values($fieldData)); + $indexes[] = " '$index' => [$properties],"; + } + foreach ($table->constraints() as $index) { + $fieldData = $table->constraint($index); + $properties = implode(', ', $this->_values($fieldData)); + $constraints[] = " '$index' => [$properties],"; + } + $options = $this->_values($table->options()); + + $content = implode("\n", $cols) . "\n"; + if (!empty($indexes)) { + $content .= " '_indexes' => [\n" . implode("\n", $indexes) . "\n ],\n"; + } + if (!empty($constraints)) { + $content .= " '_constraints' => [\n" . implode("\n", $constraints) . "\n ],\n"; + } + if (!empty($options)) { + foreach ($options as &$option) { + $option = ' ' . $option; + } + $content .= " '_options' => [\n" . implode(",\n", $options) . "\n ],\n"; + } + + return "[\n$content ]"; + } + + /** + * Formats Schema columns from Model Object + * + * @param array $values options keys(type, null, default, key, length, extra) + * @return array Formatted values + */ + protected function _values($values) + { + $vals = []; + if (!is_array($values)) { + return $vals; + } + foreach ($values as $key => $val) { + if (is_array($val)) { + $vals[] = "'{$key}' => [" . implode(", ", $this->_values($val)) . "]"; + } else { + $val = var_export($val, true); + if ($val === 'NULL') { + $val = 'null'; + } + if (!is_numeric($key)) { + $vals[] = "'{$key}' => {$val}"; + } else { + $vals[] = "{$val}"; + } + } + } + + return $vals; + } + + /** + * Generate String representation of Records + * + * @param \Cake\Database\Schema\Table $table Table schema array + * @param int $recordCount The number of records to generate. + * @return array Array of records to use in the fixture. + */ + protected function _generateRecords(Table $table, $recordCount = 1) + { + $records = []; + for ($i = 0; $i < $recordCount; $i++) { + $record = []; + foreach ($table->columns() as $field) { + $fieldInfo = $table->column($field); + $insert = ''; + switch ($fieldInfo['type']) { + case 'decimal': + $insert = $i + 1.5; + break; + case 'biginteger': + case 'integer': + case 'float': + case 'smallinteger': + case 'tinyinteger': + $insert = $i + 1; + break; + case 'string': + case 'binary': + $isPrimary = in_array($field, $table->primaryKey()); + if ($isPrimary) { + $insert = Text::uuid(); + } else { + $insert = "Lorem ipsum dolor sit amet"; + if (!empty($fieldInfo['length'])) { + $insert = substr($insert, 0, (int)$fieldInfo['length'] - 2); + } + } + break; + case 'timestamp': + $insert = time(); + break; + case 'datetime': + $insert = date('Y-m-d H:i:s'); + break; + case 'date': + $insert = date('Y-m-d'); + break; + case 'time': + $insert = date('H:i:s'); + break; + case 'boolean': + $insert = 1; + break; + case 'text': + $insert = "Lorem ipsum dolor sit amet, aliquet feugiat."; + $insert .= " Convallis morbi fringilla gravida,"; + $insert .= " phasellus feugiat dapibus velit nunc, pulvinar eget sollicitudin"; + $insert .= " venenatis cum nullam, vivamus ut a sed, mollitia lectus. Nulla"; + $insert .= " vestibulum massa neque ut et, id hendrerit sit,"; + $insert .= " feugiat in taciti enim proin nibh, tempor dignissim, rhoncus"; + $insert .= " duis vestibulum nunc mattis convallis."; + break; + case 'uuid': + $insert = Text::uuid(); + break; + } + $record[$field] = $insert; + } + $records[] = $record; + } + + return $records; + } + + /** + * Convert a $records array into a string. + * + * @param array $records Array of records to be converted to string + * @return string A string value of the $records array. + */ + protected function _makeRecordString($records) + { + $out = "[\n"; + foreach ($records as $record) { + $values = []; + foreach ($record as $field => $value) { + if ($value instanceof DateTimeInterface) { + $value = $value->format('Y-m-d H:i:s'); + } + $val = var_export($value, true); + if ($val === 'NULL') { + $val = 'null'; + } + $values[] = " '$field' => $val"; + } + $out .= " [\n"; + $out .= implode(",\n", $values); + $out .= "\n ],\n"; + } + $out .= " ]"; + + return $out; + } + + /** + * Interact with the user to get a custom SQL condition and use that to extract data + * to build a fixture. + * + * @param string $modelName name of the model to take records from. + * @param string|null $useTable Name of table to use. + * @return array Array of records. + */ + protected function _getRecordsFromTable($modelName, $useTable = null) + { + $recordCount = (isset($this->params['count']) ? $this->params['count'] : 10); + $conditions = (isset($this->params['conditions']) ? $this->params['conditions'] : '1=1'); + if (TableRegistry::exists($modelName)) { + $model = TableRegistry::get($modelName); + } else { + $model = TableRegistry::get($modelName, [ + 'table' => $useTable, + 'connection' => ConnectionManager::get($this->connection) + ]); + } + $records = $model->find('all') + ->where($conditions) + ->limit($recordCount) + ->enableHydration(false); + + return $records; + } +} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/FormTask.php b/app/vendor/cakephp/bake/src/Shell/Task/FormTask.php new file mode 100644 index 000000000..d9cd3bce0 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/Task/FormTask.php @@ -0,0 +1,55 @@ +bakeLayouts($name); + + return parent::bake($name); + } + + /** + * Bake empty layout files for html/text emails. + * + * @param string $name The name of the mailer layouts are needed for. + * @return void + */ + public function bakeLayouts($name) + { + $restore = $this->pathFragment; + $layoutsPath = implode(DS, ['Template', 'Layout', 'Email']); + + foreach (['html', 'text'] as $type) { + $this->pathFragment = implode(DS, [$layoutsPath, $type, Inflector::underscore($name) . '.ctp']); + $path = $this->getPath(); + $this->createFile($path, ''); + } + + $this->pathFragment = $restore; + } +} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/MiddlewareTask.php b/app/vendor/cakephp/bake/src/Shell/Task/MiddlewareTask.php new file mode 100644 index 000000000..3d6a3010b --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/Task/MiddlewareTask.php @@ -0,0 +1,55 @@ +_getName($name); + + if (empty($name)) { + $this->out('Choose a model to bake from the following:'); + foreach ($this->listUnskipped() as $table) { + $this->out('- ' . $this->_camelize($table)); + } + + return; + } + + $this->bake($this->_camelize($name)); + } + + /** + * Generate code for the given model name. + * + * @param string $name The model name to generate. + * @return void + */ + public function bake($name) + { + $table = $this->getTable($name); + $tableObject = $this->getTableObject($name, $table); + $data = $this->getTableContext($tableObject, $table, $name); + $this->bakeTable($tableObject, $data); + $this->bakeEntity($tableObject, $data); + $this->bakeFixture($tableObject->getAlias(), $tableObject->getTable()); + $this->bakeTest($tableObject->getAlias()); + } + + /** + * Get table context for baking a given table. + * + * @param \Cake\ORM\Table $tableObject The model name to generate. + * @param string $table The table name for the model being baked. + * @param string $name The model name to generate. + * @return array + */ + public function getTableContext($tableObject, $table, $name) + { + $associations = $this->getAssociations($tableObject); + $this->applyAssociations($tableObject, $associations); + $associationInfo = $this->getAssociationInfo($tableObject); + + $primaryKey = $this->getPrimaryKey($tableObject); + $displayField = $this->getDisplayField($tableObject); + $propertySchema = $this->getEntityPropertySchema($tableObject); + $fields = $this->getFields($tableObject); + $validation = $this->getValidation($tableObject, $associations); + $rulesChecker = $this->getRules($tableObject, $associations); + $behaviors = $this->getBehaviors($tableObject); + $connection = $this->connection; + $hidden = $this->getHiddenFields($tableObject); + + return compact( + 'associations', + 'associationInfo', + 'primaryKey', + 'displayField', + 'table', + 'propertySchema', + 'fields', + 'validation', + 'rulesChecker', + 'behaviors', + 'connection', + 'hidden' + ); + } + + /** + * Bake all models at once. + * + * @return void + */ + public function all() + { + $tables = $this->listUnskipped(); + foreach ($tables as $table) { + TableRegistry::clear(); + $this->main($table); + } + } + + /** + * Get a model object for a class name. + * + * @param string $className Name of class you want model to be. + * @param string $table Table name + * @return \Cake\ORM\Table Table instance + */ + public function getTableObject($className, $table) + { + $plugin = $this->param('plugin'); + if (!empty($plugin)) { + $className = $plugin . '.' . $className; + } + + if (TableRegistry::exists($className)) { + return TableRegistry::get($className); + } + + return TableRegistry::get($className, [ + 'name' => $className, + 'table' => $this->tablePrefix . $table, + 'connection' => ConnectionManager::get($this->connection) + ]); + } + + /** + * Get the array of associations to generate. + * + * @param \Cake\ORM\Table $table The table to get associations for. + * @return array + */ + public function getAssociations(Table $table) + { + if (!empty($this->params['no-associations'])) { + return []; + } + $this->out('One moment while associations are detected.'); + + $this->listAll(); + + $associations = [ + 'belongsTo' => [], + 'hasMany' => [], + 'belongsToMany' => [] + ]; + + $primary = $table->getPrimaryKey(); + $associations = $this->findBelongsTo($table, $associations); + + if (is_array($primary) && count($primary) > 1) { + $this->err( + 'Bake cannot generate associations for composite primary keys at this time.' + ); + + return $associations; + } + + $associations = $this->findHasMany($table, $associations); + $associations = $this->findBelongsToMany($table, $associations); + + return $associations; + } + + /** + * Sync the in memory table object. + * + * Composer's class cache prevents us from loading the + * newly generated class. Applying associations if we have a + * generic table object means fields will be detected correctly. + * + * @param \Cake\ORM\Table $model The table to apply associations to. + * @param array $associations The associations to append. + * @return void + */ + public function applyAssociations($model, $associations) + { + if (get_class($model) !== 'Cake\ORM\Table') { + return; + } + foreach ($associations as $type => $assocs) { + foreach ($assocs as $assoc) { + $alias = $assoc['alias']; + unset($assoc['alias']); + $model->{$type}($alias, $assoc); + } + } + } + + /** + * Collects meta information for associations. + * + * The information returned is in the format of map, where the key is the + * association alias: + * + * ``` + * [ + * 'associationAlias' => [ + * 'targetFqn' => '...' + * ], + * // ... + * ] + * ``` + * + * @param \Cake\ORM\Table $table The table from which to collect association information. + * @return array A map of association information. + */ + public function getAssociationInfo(Table $table) + { + $info = []; + + $appNamespace = Configure::read('App.namespace'); + + foreach ($table->associations() as $association) { + /* @var $association \Cake\ORM\Association */ + + $tableClass = get_class($association->getTarget()); + if ($tableClass === 'Cake\ORM\Table') { + $namespace = $appNamespace; + + $className = $association->className(); + if ($className !== null) { + list($plugin, $className) = pluginSplit($className); + if ($plugin !== null) { + $namespace = $plugin; + } + } else { + $className = $association->getTarget()->getAlias(); + } + + $namespace = str_replace('/', '\\', trim($namespace, '\\')); + $tableClass = $namespace . '\Model\Table\\' . $className . 'Table'; + } + + $info[$association->getName()] = [ + 'targetFqn' => '\\' . $tableClass + ]; + } + + return $info; + } + + /** + * Find belongsTo relations and add them to the associations list. + * + * @param \Cake\ORM\Table $model Database\Table instance of table being generated. + * @param array $associations Array of in progress associations + * @return array Associations with belongsTo added in. + */ + public function findBelongsTo($model, array $associations) + { + $schema = $model->getSchema(); + foreach ($schema->columns() as $fieldName) { + if (!preg_match('/^.+_id$/', $fieldName) || ([$fieldName] === $schema->primaryKey())) { + continue; + } + + if ($fieldName === 'parent_id') { + $className = ($this->plugin) ? $this->plugin . '.' . $model->getAlias() : $model->getAlias(); + $assoc = [ + 'alias' => 'Parent' . $model->getAlias(), + 'className' => $className, + 'foreignKey' => $fieldName + ]; + } else { + $tmpModelName = $this->_modelNameFromKey($fieldName); + if (!in_array(Inflector::tableize($tmpModelName), $this->_tables)) { + $found = $this->findTableReferencedBy($schema, $fieldName); + if ($found) { + $tmpModelName = Inflector::camelize($found); + } + } + $assoc = [ + 'alias' => $tmpModelName, + 'foreignKey' => $fieldName + ]; + if ($schema->getColumn($fieldName)['null'] === false) { + $assoc['joinType'] = 'INNER'; + } + } + + if ($this->plugin && empty($assoc['className'])) { + $assoc['className'] = $this->plugin . '.' . $assoc['alias']; + } + $associations['belongsTo'][] = $assoc; + } + + return $associations; + } + + /** + * find the table, if any, actually referenced by the passed key field. + * Search tables in db for keyField; if found search key constraints + * for the table to which it refers. + * + * @param \Cake\Database\Schema\TableSchema $schema The table schema to find a constraint for. + * @param string $keyField The field to check for a constraint. + * @return string|null Either the referenced table or null if the field has no constraints. + */ + public function findTableReferencedBy($schema, $keyField) + { + if (!$schema->getColumn($keyField)) { + return null; + } + + foreach ($schema->constraints() as $constraint) { + $constraintInfo = $schema->getConstraint($constraint); + if (!in_array($keyField, $constraintInfo['columns'])) { + continue; + } + + if (!isset($constraintInfo['references'])) { + continue; + } + $length = mb_strlen($this->tablePrefix); + if ($length > 0 && mb_substr($constraintInfo['references'][0], 0, $length) === $this->tablePrefix) { + return mb_substr($constraintInfo['references'][0], $length); + } + + return $constraintInfo['references'][0]; + } + + return null; + } + + /** + * Find the hasMany relations and add them to associations list + * + * @param \Cake\ORM\Table $model Model instance being generated + * @param array $associations Array of in progress associations + * @return array Associations with hasMany added in. + */ + public function findHasMany($model, array $associations) + { + $schema = $model->getSchema(); + $primaryKey = (array)$schema->primaryKey(); + $tableName = $schema->name(); + $foreignKey = $this->_modelKey($tableName); + + $tables = $this->listAll(); + foreach ($tables as $otherTableName) { + $otherModel = $this->getTableObject($this->_camelize($otherTableName), $otherTableName); + $otherSchema = $otherModel->getSchema(); + + $pregTableName = preg_quote($tableName, '/'); + $pregPattern = "/^{$pregTableName}_|_{$pregTableName}$/"; + if (preg_match($pregPattern, $otherTableName) === 1) { + $possibleHABTMTargetTable = preg_replace($pregPattern, '', $otherTableName); + if (in_array($possibleHABTMTargetTable, $tables)) { + continue; + } + } + + foreach ($otherSchema->columns() as $fieldName) { + $assoc = false; + if (!in_array($fieldName, $primaryKey) && $fieldName === $foreignKey) { + $assoc = [ + 'alias' => $otherModel->getAlias(), + 'foreignKey' => $fieldName + ]; + } elseif ($otherTableName === $tableName && $fieldName === 'parent_id') { + $className = ($this->plugin) ? $this->plugin . '.' . $model->getAlias() : $model->getAlias(); + $assoc = [ + 'alias' => 'Child' . $model->getAlias(), + 'className' => $className, + 'foreignKey' => $fieldName + ]; + } + if ($assoc && $this->plugin && empty($assoc['className'])) { + $assoc['className'] = $this->plugin . '.' . $assoc['alias']; + } + if ($assoc) { + $associations['hasMany'][] = $assoc; + } + } + } + + return $associations; + } + + /** + * Find the BelongsToMany relations and add them to associations list + * + * @param \Cake\ORM\Table $model Model instance being generated + * @param array $associations Array of in-progress associations + * @return array Associations with belongsToMany added in. + */ + public function findBelongsToMany($model, array $associations) + { + $schema = $model->getSchema(); + $tableName = $schema->name(); + $foreignKey = $this->_modelKey($tableName); + + $tables = $this->listAll(); + foreach ($tables as $otherTableName) { + $assocTable = null; + $offset = strpos($otherTableName, $tableName . '_'); + $otherOffset = strpos($otherTableName, '_' . $tableName); + + if ($offset !== false) { + $assocTable = substr($otherTableName, strlen($tableName . '_')); + } elseif ($otherOffset !== false) { + $assocTable = substr($otherTableName, 0, $otherOffset); + } + if ($assocTable && in_array($assocTable, $tables)) { + $habtmName = $this->_camelize($assocTable); + $assoc = [ + 'alias' => $habtmName, + 'foreignKey' => $foreignKey, + 'targetForeignKey' => $this->_modelKey($habtmName), + 'joinTable' => $otherTableName + ]; + if ($assoc && $this->plugin) { + $assoc['className'] = $this->plugin . '.' . $assoc['alias']; + } + $associations['belongsToMany'][] = $assoc; + } + } + + return $associations; + } + + /** + * Get the display field from the model or parameters + * + * @param \Cake\ORM\Table $model The model to introspect. + * @return string + */ + public function getDisplayField($model) + { + if (!empty($this->params['display-field'])) { + return $this->params['display-field']; + } + + return $model->getDisplayField(); + } + + /** + * Get the primary key field from the model or parameters + * + * @param \Cake\ORM\Table $model The model to introspect. + * @return array The columns in the primary key + */ + public function getPrimaryKey($model) + { + if (!empty($this->params['primary-key'])) { + $fields = explode(',', $this->params['primary-key']); + + return array_values(array_filter(array_map('trim', $fields))); + } + + return (array)$model->getPrimaryKey(); + } + + /** + * Returns an entity property "schema". + * + * The schema is an associative array, using the property names + * as keys, and information about the property as the value. + * + * The value part consists of at least two keys: + * + * - `kind`: The kind of property, either `column`, which indicates + * that the property stems from a database column, or `association`, + * which identifies a property that is generated for an associated + * table. + * - `type`: The type of the property value. For the `column` kind + * this is the database type associated with the column, and for the + * `association` type it's the FQN of the entity class for the + * associated table. + * + * For `association` properties an additional key will be available + * + * - `association`: Holds an instance of the corresponding association + * class. + * + * @param \Cake\ORM\Table $model The model to introspect. + * @return array The property schema + */ + public function getEntityPropertySchema(Table $model) + { + $properties = []; + + $schema = $model->getSchema(); + foreach ($schema->columns() as $column) { + $properties[$column] = [ + 'kind' => 'column', + 'type' => $schema->getColumnType($column) + ]; + } + + foreach ($model->associations() as $association) { + $entityClass = '\\' . ltrim($association->getTarget()->getEntityClass(), '\\'); + + if ($entityClass === '\Cake\ORM\Entity') { + $namespace = Configure::read('App.namespace'); + + list($plugin, ) = pluginSplit($association->getTarget()->getRegistryAlias()); + if ($plugin !== null) { + $namespace = $plugin; + } + $namespace = str_replace('/', '\\', trim($namespace, '\\')); + + $entityClass = $this->_entityName($association->getTarget()->getAlias()); + $entityClass = '\\' . $namespace . '\Model\Entity\\' . $entityClass; + } + + $properties[$association->getProperty()] = [ + 'kind' => 'association', + 'association' => $association, + 'type' => $entityClass + ]; + } + + return $properties; + } + + /** + * Evaluates the fields and no-fields options, and + * returns if, and which fields should be made accessible. + * + * If no fields are specified and the `no-fields` parameter is + * not set, then all non-primary key fields + association + * fields will be set as accessible. + * + * @param \Cake\ORM\Table $table The table instance to get fields for. + * @return array|bool|null Either an array of fields, `false` in + * case the no-fields option is used, or `null` if none of the + * field options is used. + */ + public function getFields($table) + { + if (!empty($this->params['no-fields'])) { + return false; + } + if (!empty($this->params['fields'])) { + $fields = explode(',', $this->params['fields']); + + return array_values(array_filter(array_map('trim', $fields))); + } + $schema = $table->getSchema(); + $fields = $schema->columns(); + foreach ($table->associations() as $assoc) { + $fields[] = $assoc->getProperty(); + } + $primaryKey = $schema->primaryKey(); + + return array_values(array_diff($fields, $primaryKey)); + } + + /** + * Get the hidden fields from a model. + * + * Uses the hidden and no-hidden options. + * + * @param \Cake\ORM\Table $model The model to introspect. + * @return array The columns to make accessible + */ + public function getHiddenFields($model) + { + if (!empty($this->params['no-hidden'])) { + return []; + } + if (!empty($this->params['hidden'])) { + $fields = explode(',', $this->params['hidden']); + + return array_values(array_filter(array_map('trim', $fields))); + } + $schema = $model->getSchema(); + $columns = $schema->columns(); + $whitelist = ['token', 'password', 'passwd']; + + return array_values(array_intersect($columns, $whitelist)); + } + + /** + * Generate default validation rules. + * + * @param \Cake\ORM\Table $model The model to introspect. + * @param array $associations The associations list. + * @return array The validation rules. + */ + public function getValidation($model, $associations = []) + { + if (!empty($this->params['no-validation'])) { + return []; + } + $schema = $model->getSchema(); + $fields = $schema->columns(); + if (empty($fields)) { + return false; + } + + $validate = []; + $primaryKey = (array)$schema->primaryKey(); + $foreignKeys = []; + if (isset($associations['belongsTo'])) { + foreach ($associations['belongsTo'] as $assoc) { + $foreignKeys[] = $assoc['foreignKey']; + } + } + foreach ($fields as $fieldName) { + if (in_array($fieldName, $foreignKeys)) { + continue; + } + $field = $schema->getColumn($fieldName); + $validation = $this->fieldValidation($schema, $fieldName, $field, $primaryKey); + if (!empty($validation)) { + $validate[$fieldName] = $validation; + } + } + + return $validate; + } + + /** + * Does individual field validation handling. + * + * @param \Cake\Database\Schema\TableSchema $schema The table schema for the current field. + * @param string $fieldName Name of field to be validated. + * @param array $metaData metadata for field + * @param array $primaryKey The primary key field + * @return array Array of validation for the field. + */ + public function fieldValidation($schema, $fieldName, array $metaData, $primaryKey) + { + $ignoreFields = ['lft', 'rght', 'created', 'modified', 'updated']; + if (in_array($fieldName, $ignoreFields)) { + return []; + } + + $rules = []; + if ($fieldName === 'email') { + $rules['email'] = []; + } elseif ($metaData['type'] === 'uuid') { + $rules['uuid'] = []; + } elseif ($metaData['type'] === 'integer') { + $rules['integer'] = []; + } elseif ($metaData['type'] === 'float') { + $rules['numeric'] = []; + } elseif ($metaData['type'] === 'decimal') { + $rules['decimal'] = []; + } elseif ($metaData['type'] === 'boolean') { + $rules['boolean'] = []; + } elseif ($metaData['type'] === 'date') { + $rules['date'] = []; + } elseif ($metaData['type'] === 'time') { + $rules['time'] = []; + } elseif ($metaData['type'] === 'datetime') { + $rules['dateTime'] = []; + } elseif ($metaData['type'] === 'timestamp') { + $rules['dateTime'] = []; + } elseif ($metaData['type'] === 'inet') { + $rules['ip'] = []; + } elseif ($metaData['type'] === 'string' || $metaData['type'] === 'text') { + $rules['scalar'] = []; + if ($metaData['length'] > 0) { + $rules['maxLength'] = [$metaData['length']]; + } + } + + if (in_array($fieldName, (array)$primaryKey)) { + $rules['allowEmpty'] = ["'create'"]; + } elseif ($metaData['null'] === true) { + $rules['allowEmpty'] = []; + } else { + $rules['requirePresence'] = ["'create'"]; + $rules['notEmpty'] = []; + } + + $validation = []; + foreach ($rules as $rule => $args) { + $validation[$rule] = [ + 'rule' => $rule, + 'args' => $args + ]; + } + + foreach ($schema->constraints() as $constraint) { + $constraint = $schema->getConstraint($constraint); + if (!in_array($fieldName, $constraint['columns']) || count($constraint['columns']) > 1) { + continue; + } + + $notDatetime = !in_array($metaData['type'], ['datetime', 'timestamp', 'date', 'time']); + if ($constraint['type'] === TableSchema::CONSTRAINT_UNIQUE && $notDatetime) { + $validation['unique'] = ['rule' => 'validateUnique', 'provider' => 'table']; + } + } + + return $validation; + } + + /** + * Generate default rules checker. + * + * @param \Cake\ORM\Table $model The model to introspect. + * @param array $associations The associations for the model. + * @return array The rules to be applied. + */ + public function getRules($model, array $associations) + { + if (!empty($this->params['no-rules'])) { + return []; + } + $schema = $model->getSchema(); + $fields = $schema->columns(); + if (empty($fields)) { + return []; + } + + $rules = []; + foreach ($fields as $fieldName) { + if (in_array($fieldName, ['username', 'email', 'login'])) { + $rules[$fieldName] = ['name' => 'isUnique']; + } + } + foreach ($schema->constraints() as $name) { + $constraint = $schema->getConstraint($name); + if ($constraint['type'] !== TableSchema::CONSTRAINT_UNIQUE) { + continue; + } + if (count($constraint['columns']) > 1) { + continue; + } + $rules[$constraint['columns'][0]] = ['name' => 'isUnique']; + } + + if (empty($associations['belongsTo'])) { + return $rules; + } + + foreach ($associations['belongsTo'] as $assoc) { + $rules[$assoc['foreignKey']] = ['name' => 'existsIn', 'extra' => $assoc['alias']]; + } + + return $rules; + } + + /** + * Get behaviors + * + * @param \Cake\ORM\Table $model The model to generate behaviors for. + * @return array Behaviors + */ + public function getBehaviors($model) + { + $behaviors = []; + $schema = $model->getSchema(); + $fields = $schema->columns(); + if (empty($fields)) { + return []; + } + if (in_array('created', $fields) || in_array('modified', $fields)) { + $behaviors['Timestamp'] = []; + } + + if (in_array('lft', $fields) && $schema->getColumnType('lft') === 'integer' && + in_array('rght', $fields) && $schema->getColumnType('rght') === 'integer' && + in_array('parent_id', $fields) + ) { + $behaviors['Tree'] = []; + } + + $counterCache = $this->getCounterCache($model); + if (!empty($counterCache)) { + $behaviors['CounterCache'] = $counterCache; + } + + return $behaviors; + } + + /** + * Get CounterCaches + * + * @param \Cake\ORM\Table $model The table to get counter cache fields for. + * @return array CounterCache configurations + */ + public function getCounterCache($model) + { + $belongsTo = $this->findBelongsTo($model, ['belongsTo' => []]); + $counterCache = []; + foreach ($belongsTo['belongsTo'] as $otherTable) { + $otherAlias = $otherTable['alias']; + $otherModel = $this->getTableObject($this->_camelize($otherAlias), Inflector::underscore($otherAlias)); + + try { + $otherSchema = $otherModel->getSchema(); + } catch (\Cake\Database\Exception $e) { + continue; + } + + $otherFields = $otherSchema->columns(); + $alias = $model->getAlias(); + $field = Inflector::singularize(Inflector::underscore($alias)) . '_count'; + if (in_array($field, $otherFields, true)) { + $counterCache[] = "'{$otherAlias}' => ['{$field}']"; + } + } + + return $counterCache; + } + + /** + * Bake an entity class. + * + * @param \Cake\ORM\Table $model Model name or object + * @param array $data An array to use to generate the Table + * @return string|null + */ + public function bakeEntity($model, array $data = []) + { + if (!empty($this->params['no-entity'])) { + return null; + } + $name = $this->_entityName($model->getAlias()); + + $namespace = Configure::read('App.namespace'); + $pluginPath = ''; + if ($this->plugin) { + $namespace = $this->_pluginNamespace($this->plugin); + $pluginPath = $this->plugin . '.'; + } + + $data += [ + 'name' => $name, + 'namespace' => $namespace, + 'plugin' => $this->plugin, + 'pluginPath' => $pluginPath, + 'primaryKey' => [] + ]; + + $this->BakeTemplate->set($data); + $out = $this->BakeTemplate->generate('Model/entity'); + + $path = $this->getPath(); + $filename = $path . 'Entity' . DS . $name . '.php'; + $this->out("\n" . sprintf('Baking entity class for %s...', $name), 1, Shell::QUIET); + $this->createFile($filename, $out); + $emptyFile = $path . 'Entity' . DS . 'empty'; + $this->_deleteEmptyFile($emptyFile); + + return $out; + } + + /** + * Bake a table class. + * + * @param \Cake\ORM\Table $model Model name or object + * @param array $data An array to use to generate the Table + * @return string|null + */ + public function bakeTable($model, array $data = []) + { + if (!empty($this->params['no-table'])) { + return null; + } + + $namespace = Configure::read('App.namespace'); + $pluginPath = ''; + if ($this->plugin) { + $namespace = $this->_pluginNamespace($this->plugin); + } + + $name = $model->getAlias(); + $entity = $this->_entityName($model->getAlias()); + $data += [ + 'plugin' => $this->plugin, + 'pluginPath' => $pluginPath, + 'namespace' => $namespace, + 'name' => $name, + 'entity' => $entity, + 'associations' => [], + 'primaryKey' => 'id', + 'displayField' => null, + 'table' => null, + 'validation' => [], + 'rulesChecker' => [], + 'behaviors' => [], + 'connection' => $this->connection, + ]; + + $this->BakeTemplate->set($data); + $out = $this->BakeTemplate->generate('Model/table'); + + $path = $this->getPath(); + $filename = $path . 'Table' . DS . $name . 'Table.php'; + $this->out("\n" . sprintf('Baking table class for %s...', $name), 1, Shell::QUIET); + $this->createFile($filename, $out); + + // Work around composer caching that classes/files do not exist. + // Check for the file as it might not exist in tests. + if (file_exists($filename)) { + require_once $filename; + } + TableRegistry::clear(); + + $emptyFile = $path . 'Table' . DS . 'empty'; + $this->_deleteEmptyFile($emptyFile); + + return $out; + } + + /** + * Outputs the a list of possible models or controllers from database + * + * @return array + */ + public function listAll() + { + if (!empty($this->_tables)) { + return $this->_tables; + } + + $this->_modelNames = []; + $this->_tables = $this->_getAllTables(); + foreach ($this->_tables as $table) { + $this->_modelNames[] = $this->_camelize($table); + } + + return $this->_tables; + } + + /** + * Outputs the a list of unskipped models or controllers from database + * + * @return array + */ + public function listUnskipped() + { + $this->listAll(); + + return array_diff($this->_tables, $this->skipTables); + } + + /** + * Models never have routing prefixes applied. + * + * @return string + */ + protected function _getPrefix() + { + return ''; + } + + /** + * Get an Array of all the tables in the supplied connection + * will halt the script if no tables are found. + * + * @return array Array of tables in the database. + * @throws \InvalidArgumentException When connection class + * does not have a schemaCollection method. + */ + protected function _getAllTables() + { + $db = ConnectionManager::get($this->connection); + if (!method_exists($db, 'schemaCollection')) { + $this->abort( + 'Connections need to implement schemaCollection() to be used with bake.' + ); + } + $schema = $db->schemaCollection(); + $tables = $schema->listTables(); + if (empty($tables)) { + $this->abort('Your database does not have any tables.'); + } + sort($tables); + + return $tables; + } + + /** + * Get the table name for the model being baked. + * + * Uses the `table` option if it is set. + * + * @param string $name Table name + * @return string + */ + public function getTable($name) + { + if (isset($this->params['table'])) { + return $this->params['table']; + } + + return Inflector::underscore($name); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->setDescription( + 'Bake table and entity classes.' + )->addArgument('name', [ + 'help' => 'Name of the model to bake (without the Table suffix). ' . + 'You can use Plugin.name to bake plugin models.' + ])->addSubcommand('all', [ + 'help' => 'Bake all model files with associations and validation.' + ])->addOption('table', [ + 'help' => 'The table name to use if you have non-conventional table names.' + ])->addOption('no-entity', [ + 'boolean' => true, + 'help' => 'Disable generating an entity class.' + ])->addOption('no-table', [ + 'boolean' => true, + 'help' => 'Disable generating a table class.' + ])->addOption('no-validation', [ + 'boolean' => true, + 'help' => 'Disable generating validation rules.' + ])->addOption('no-rules', [ + 'boolean' => true, + 'help' => 'Disable generating a rules checker.' + ])->addOption('no-associations', [ + 'boolean' => true, + 'help' => 'Disable generating associations.' + ])->addOption('no-fields', [ + 'boolean' => true, + 'help' => 'Disable generating accessible fields in the entity.' + ])->addOption('fields', [ + 'help' => 'A comma separated list of fields to make accessible.' + ])->addOption('no-hidden', [ + 'boolean' => true, + 'help' => 'Disable generating hidden fields in the entity.' + ])->addOption('hidden', [ + 'help' => 'A comma separated list of fields to hide.' + ])->addOption('primary-key', [ + 'help' => 'The primary key if you would like to manually set one.' . + ' Can be a comma separated list if you are using a composite primary key.' + ])->addOption('display-field', [ + 'help' => 'The displayField if you would like to choose one.' + ])->addOption('no-test', [ + 'boolean' => true, + 'help' => 'Do not generate a test case skeleton.' + ])->addOption('no-fixture', [ + 'boolean' => true, + 'help' => 'Do not generate a test fixture skeleton.' + ])->setEpilog( + 'Omitting all arguments and options will list the table names you can generate models for' + ); + + return $parser; + } + + /** + * Interact with FixtureTask to automatically bake fixtures when baking models. + * + * @param string $className Name of class to bake fixture for + * @param string|null $useTable Optional table name for fixture to use. + * @return void + * @see FixtureTask::bake + */ + public function bakeFixture($className, $useTable = null) + { + if (!empty($this->params['no-fixture'])) { + return; + } + $this->Fixture->connection = $this->connection; + $this->Fixture->plugin = $this->plugin; + $this->Fixture->interactive = $this->interactive; + $this->Fixture->bake($className, $useTable); + } + + /** + * Assembles and writes a unit test file + * + * @param string $className Model class name + * @return string|null + */ + public function bakeTest($className) + { + if (!empty($this->params['no-test'])) { + return null; + } + $this->Test->plugin = $this->plugin; + $this->Test->interactive = $this->interactive; + $this->Test->connection = $this->connection; + + return $this->Test->bake('Table', $className); + } +} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/PluginTask.php b/app/vendor/cakephp/bake/src/Shell/Task/PluginTask.php new file mode 100644 index 000000000..5e00e47b8 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/Task/PluginTask.php @@ -0,0 +1,410 @@ +path = current(App::path('Plugin')); + $this->bootstrap = ROOT . DS . 'config' . DS . 'bootstrap.php'; + } + + /** + * Execution method always used for tasks + * + * @param string|null $name The name of the plugin to bake. + * @return null|bool + */ + public function main($name = null) + { + if (empty($name)) { + $this->err('You must provide a plugin name in CamelCase format.'); + $this->err('To make an "MyExample" plugin, run `cake bake plugin MyExample`.'); + + return false; + } + $parts = explode('/', $name); + $plugin = implode('/', array_map([$this, '_camelize'], $parts)); + + $pluginPath = $this->_pluginPath($plugin); + if (is_dir($pluginPath)) { + $this->out(sprintf('Plugin: %s already exists, no action taken', $plugin)); + $this->out(sprintf('Path: %s', $pluginPath)); + + return false; + } + if (!$this->bake($plugin)) { + $this->error(sprintf("An error occurred trying to bake: %s in %s", $plugin, $this->path . $plugin)); + } + } + + /** + * Bake the plugin's contents + * + * Also update the autoloader and the root composer.json file if it can be found + * + * @param string $plugin Name of the plugin in CamelCased format + * @return bool|null + */ + public function bake($plugin) + { + $pathOptions = App::path('Plugin'); + if (count($pathOptions) > 1) { + $this->findPath($pathOptions); + } + $this->out(sprintf("Plugin Name: %s", $plugin)); + $this->out(sprintf("Plugin Directory: %s", $this->path . $plugin)); + $this->hr(); + + $looksGood = $this->in('Look okay?', ['y', 'n', 'q'], 'y'); + + if (strtolower($looksGood) !== 'y') { + return null; + } + + $this->_generateFiles($plugin, $this->path); + + $hasAutoloader = $this->_modifyAutoloader($plugin, $this->path); + $this->_modifyBootstrap($plugin, $hasAutoloader); + + $this->hr(); + $this->out(sprintf('Created: %s in %s', $plugin, $this->path . $plugin), 2); + + $emptyFile = $this->path . 'empty'; + $this->_deleteEmptyFile($emptyFile); + + return true; + } + + /** + * Update the app's bootstrap.php file. + * + * @param string $plugin Name of plugin + * @param bool $hasAutoloader Whether or not there is an autoloader configured for + * the plugin + * @return void + */ + protected function _modifyBootstrap($plugin, $hasAutoloader) + { + $bootstrap = new File($this->bootstrap, false); + if (!$bootstrap->exists()) { + $this->err('Could not update application bootstrap.php file, as it could not be found.'); + + return; + } + $contents = $bootstrap->read(); + if (!preg_match("@\n\s*Plugin::loadAll@", $contents)) { + $autoload = $hasAutoloader ? null : "'autoload' => true, "; + $bootstrap->append(sprintf( + "\nPlugin::load('%s', [%s'bootstrap' => false, 'routes' => true]);\n", + $plugin, + $autoload + )); + $this->out(''); + $this->out(sprintf('%s modified', $this->bootstrap)); + } + } + + /** + * Generate all files for a plugin + * + * Find the first path which contains `src/Template/Bake/Plugin` that contains + * something, and use that as the template to recursively render a plugin's + * contents. Allows the creation of a bake them containing a `Plugin` folder + * to provide customized bake output for plugins. + * + * @param string $pluginName the CamelCase name of the plugin + * @param string $path the path to the plugins dir (the containing folder) + * @return void + */ + protected function _generateFiles($pluginName, $path) + { + $namespace = str_replace('/', '\\', $pluginName); + $baseNamespace = Configure::read('App.namespace'); + + $name = $pluginName; + $vendor = 'your-name-here'; + if (strpos($pluginName, '/') !== false) { + list($vendor, $name) = explode('/', $pluginName); + } + $package = $vendor . '/' . $name; + + $this->BakeTemplate->set([ + 'package' => $package, + 'namespace' => $namespace, + 'baseNamespace' => $baseNamespace, + 'plugin' => $pluginName, + 'routePath' => Inflector::dasherize($pluginName), + 'path' => $path, + 'root' => ROOT, + ]); + + $root = $path . $pluginName . DS; + + $paths = []; + if (!empty($this->params['theme'])) { + $paths[] = Plugin::path($this->params['theme']) . 'src/Template/'; + } + + $paths = array_merge($paths, Configure::read('App.paths.templates')); + $paths[] = Plugin::path('Bake') . 'src/Template/'; + + do { + $templatesPath = array_shift($paths) . 'Bake/Plugin'; + $templatesDir = new Folder($templatesPath); + $templates = $templatesDir->findRecursive('.*\.(twig|ctp)'); + } while (!$templates); + + sort($templates); + foreach ($templates as $template) { + $template = substr($template, strrpos($template, 'Plugin') + 7, -4); + $template = rtrim($template, '.'); + $this->_generateFile($template, $root); + } + } + + /** + * Generate a file + * + * @param string $template The template to render + * @param string $root The path to the plugin's root + * @return void + */ + protected function _generateFile($template, $root) + { + $this->out(sprintf('Generating %s file...', $template)); + $out = $this->BakeTemplate->generate('Plugin/' . $template); + $this->createFile($root . $template, $out); + } + + /** + * Modifies App's composer.json to include the plugin and tries to call + * composer dump-autoload to refresh the autoloader cache + * + * @param string $plugin Name of plugin + * @param string $path The path to save the phpunit.xml file to. + * @return bool True if composer could be modified correctly + */ + protected function _modifyAutoloader($plugin, $path) + { + $file = $this->_rootComposerFilePath(); + + if (!file_exists($file)) { + $this->out(sprintf('Main composer file %s not found', $file)); + + return false; + } + + $autoloadPath = str_replace(ROOT, '.', $this->path); + $autoloadPath = str_replace('\\', '/', $autoloadPath); + $namespace = str_replace('/', '\\', $plugin); + + $config = json_decode(file_get_contents($file), true); + $config['autoload']['psr-4'][$namespace . '\\'] = $autoloadPath . $plugin . "/src"; + $config['autoload-dev']['psr-4'][$namespace . '\\Test\\'] = $autoloadPath . $plugin . "/tests"; + + $this->out('Modifying composer autoloader'); + + $out = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; + $this->createFile($file, $out); + + $composer = $this->findComposer(); + + if (!$composer) { + $this->error('Could not locate composer. Add composer to your PATH, or use the --composer option.'); + + return false; + } + + try { + $cwd = getcwd(); + + // Windows makes running multiple commands at once hard. + chdir(dirname($this->_rootComposerFilePath())); + $command = 'php ' . escapeshellarg($composer) . ' dump-autoload'; + $this->callProcess($command); + + chdir($cwd); + } catch (\RuntimeException $e) { + $error = $e->getMessage(); + $this->error(sprintf('Could not run `composer dump-autoload`: %s', $error)); + + return false; + } + + return true; + } + + /** + * The path to the main application's composer file + * + * This is a test isolation wrapper + * + * @return string the abs file path + */ + protected function _rootComposerFilePath() + { + return ROOT . DS . 'composer.json'; + } + + /** + * find and change $this->path to the user selection + * + * @param array $pathOptions The list of paths to look in. + * @return void + */ + public function findPath(array $pathOptions) + { + $valid = false; + foreach ($pathOptions as $i => $path) { + if (!is_dir($path)) { + unset($pathOptions[$i]); + } + } + $pathOptions = array_values($pathOptions); + $max = count($pathOptions); + + if ($max === 0) { + $this->err('No valid plugin paths found! Please configure a plugin path that exists.'); + throw new \RuntimeException(); + } + + if ($max === 1) { + $this->path = $pathOptions[0]; + + return; + } + + $choice = null; + while (!$valid) { + foreach ($pathOptions as $i => $option) { + $this->out($i + 1 . '. ' . $option); + } + $prompt = 'Choose a plugin path from the paths above.'; + $choice = $this->in($prompt, null, 1); + if ((int)$choice > 0 && (int)$choice <= $max) { + $valid = true; + } + } + $this->path = $pathOptions[$choice - 1]; + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + $parser->setDescription( + 'Create the directory structure, AppController class and testing setup for a new plugin. ' . + 'Can create plugins in any of your bootstrapped plugin paths.' + )->addArgument('name', [ + 'help' => 'CamelCased name of the plugin to create.' + ])->addOption('composer', [ + 'default' => ROOT . DS . 'composer.phar', + 'help' => 'The path to the composer executable.' + ])->removeOption('plugin'); + + return $parser; + } + + /** + * Uses either the CLI option or looks in $PATH and cwd for composer. + * + * @return string|false Either the path to composer or false if it cannot be found. + */ + public function findComposer() + { + if (!empty($this->params['composer'])) { + $path = $this->params['composer']; + if (file_exists($path)) { + return $path; + } + } + $composer = false; + $path = env('PATH'); + if (!empty($path)) { + $paths = explode(PATH_SEPARATOR, $path); + $composer = $this->_searchPath($paths); + } + + return $composer; + } + + /** + * Search the $PATH for composer. + * + * @param array $path The paths to search. + * @return string|bool + */ + protected function _searchPath($path) + { + $composer = ['composer.phar', 'composer']; + foreach ($path as $dir) { + foreach ($composer as $cmd) { + if (is_file($dir . DS . $cmd)) { + $this->_io->verbose('Found composer executable in ' . $dir); + + return $dir . DS . $cmd; + } + } + } + + return false; + } +} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/ShellHelperTask.php b/app/vendor/cakephp/bake/src/Shell/Task/ShellHelperTask.php new file mode 100644 index 000000000..f93891eb3 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/Task/ShellHelperTask.php @@ -0,0 +1,55 @@ +plugin) { + $namespace = $this->_pluginNamespace($this->plugin); + } + + return ['namespace' => $namespace]; + } + + /** + * Execute method + * + * @param string|null $name The name of the object to bake. + * @return int|null + */ + public function main($name = null) + { + parent::main(); + if (empty($name)) { + return $this->error('You must provide a name to bake a ' . $this->name()); + } + $name = $this->_getName($name); + $name = Inflector::camelize($name); + $this->bake($name); + $this->bakeTest($name); + } + + /** + * Generate a class stub + * + * @param string $name The classname to generate. + * @return string + */ + public function bake($name) + { + $this->BakeTemplate->set('name', $name); + $this->BakeTemplate->set($this->templateData()); + $contents = $this->BakeTemplate->generate($this->template()); + + $filename = $this->getPath() . $this->fileName($name); + $this->createFile($filename, $contents); + $emptyFile = $this->getPath() . 'empty'; + $this->_deleteEmptyFile($emptyFile); + + return $contents; + } + + /** + * Generate a test case. + * + * @param string $className The class to bake a test for. + * @return string|bool|null + */ + public function bakeTest($className) + { + if (!empty($this->params['no-test'])) { + return null; + } + $this->Test->plugin = $this->plugin; + + return $this->Test->bake($this->name(), $className); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + $name = $this->name(); + $parser->setDescription( + sprintf('Bake a %s class file.', $name) + )->addArgument('name', [ + 'help' => sprintf( + 'Name of the %s to bake. Can use Plugin.name to bake %s files into plugins.', + $name, + $name + ) + ])->addOption('no-test', [ + 'boolean' => true, + 'help' => 'Do not generate a test skeleton.' + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/TaskTask.php b/app/vendor/cakephp/bake/src/Shell/Task/TaskTask.php new file mode 100644 index 000000000..85943735e --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/Task/TaskTask.php @@ -0,0 +1,55 @@ +path = current(App::path('Template')); + } + + /** + * Execution method always used for tasks + * + * @param string|null $name The name of the controller to bake view templates for. + * @param string|null $template The template to bake with. + * @param string|null $action The output action name. Defaults to $template. + * @return mixed + */ + public function main($name = null, $template = null, $action = null) + { + parent::main(); + + if (empty($name)) { + $this->out('Possible tables to bake view templates for based on your current database:'); + $this->Model->connection = $this->connection; + foreach ($this->Model->listUnskipped() as $table) { + $this->out('- ' . $this->_camelize($table)); + } + + return true; + } + $name = $this->_getName($name); + + $controller = null; + if (!empty($this->params['controller'])) { + $controller = $this->params['controller']; + } + $this->controller($name, $controller); + $this->model($name); + + if ($template && $action === null) { + $action = $template; + } + if ($template) { + $this->bake($template, true, $action); + + return true; + } + + $vars = $this->_loadController(); + $methods = $this->_methodsToBake(); + + foreach ($methods as $method) { + $content = $this->getContent($method, $vars); + if ($content) { + $this->bake($method, $content); + } + } + } + + /** + * Set the model class for the table. + * + * @param string $table The table/model that is being baked. + * @return void + */ + public function model($table) + { + $tableName = $this->_camelize($table); + $plugin = null; + if (!empty($this->params['plugin'])) { + $plugin = $this->params['plugin'] . '.'; + } + $this->modelName = $plugin . $tableName; + } + + /** + * Set the controller related properties. + * + * @param string $table The table/model that is being baked. + * @param string|null $controller The controller name if specified. + * @return void + */ + public function controller($table, $controller = null) + { + $tableName = $this->_camelize($table); + if (empty($controller)) { + $controller = $tableName; + } + $this->controllerName = $controller; + + $plugin = $this->param('plugin'); + if ($plugin) { + $plugin .= '.'; + } + $prefix = $this->_getPrefix(); + if ($prefix) { + $prefix .= '/'; + } + $this->controllerClass = App::className($plugin . $prefix . $controller, 'Controller', 'Controller'); + } + + /** + * Get the path base for view templates. + * + * @return string + */ + public function getPath() + { + $path = parent::getPath(); + $path .= $this->controllerName . DS; + + return $path; + } + + /** + * Get a list of actions that can / should have view templates baked for them. + * + * @return array Array of action names that should be baked + */ + protected function _methodsToBake() + { + $base = Configure::read('App.namespace'); + + $methods = []; + if (class_exists($this->controllerClass)) { + $methods = array_diff( + array_map( + 'Cake\Utility\Inflector::underscore', + get_class_methods($this->controllerClass) + ), + array_map( + 'Cake\Utility\Inflector::underscore', + get_class_methods($base . '\Controller\AppController') + ) + ); + } + if (empty($methods)) { + $methods = $this->scaffoldActions; + } + foreach ($methods as $i => $method) { + if ($method[0] === '_') { + unset($methods[$i]); + } + } + + return $methods; + } + + /** + * Bake All view templates for all controller actions. + * + * @return void + */ + public function all() + { + $this->Model->connection = $this->connection; + $tables = $this->Model->listUnskipped(); + + foreach ($tables as $table) { + $this->main($table); + } + } + + /** + * Loads Controller and sets variables for the template + * Available template variables: + * + * - 'modelObject' + * - 'modelClass' + * - 'entityClass' + * - 'primaryKey' + * - 'displayField' + * - 'singularVar' + * - 'pluralVar' + * - 'singularHumanName' + * - 'pluralHumanName' + * - 'fields' + * - 'keyFields' + * - 'schema' + * + * @return array Returns variables to be made available to a view template + */ + protected function _loadController() + { + if (TableRegistry::exists($this->modelName)) { + $modelObject = TableRegistry::get($this->modelName); + } else { + $modelObject = TableRegistry::get($this->modelName, [ + 'connectionName' => $this->connection + ]); + } + + $namespace = Configure::read('App.namespace'); + + $primaryKey = (array)$modelObject->getPrimaryKey(); + $displayField = $modelObject->getDisplayField(); + $singularVar = $this->_singularName($this->controllerName); + $singularHumanName = $this->_singularHumanName($this->controllerName); + $schema = $modelObject->getSchema(); + $fields = $schema->columns(); + $modelClass = $this->modelName; + + list(, $entityClass) = namespaceSplit($this->_entityName($this->modelName)); + $entityClass = sprintf('%s\Model\Entity\%s', $namespace, $entityClass); + if (!class_exists($entityClass)) { + $entityClass = EntityInterface::class; + } + $associations = $this->_filteredAssociations($modelObject); + $keyFields = []; + if (!empty($associations['BelongsTo'])) { + foreach ($associations['BelongsTo'] as $assoc) { + $keyFields[$assoc['foreignKey']] = $assoc['variable']; + } + } + + $pluralVar = Inflector::variable($this->controllerName); + $pluralHumanName = $this->_pluralHumanName($this->controllerName); + + return compact( + 'modelObject', + 'modelClass', + 'entityClass', + 'schema', + 'primaryKey', + 'displayField', + 'singularVar', + 'pluralVar', + 'singularHumanName', + 'pluralHumanName', + 'fields', + 'associations', + 'keyFields', + 'namespace' + ); + } + + /** + * Assembles and writes bakes the view file. + * + * @param string $template Template file to use. + * @param string $content Content to write. + * @param string $outputFile The output file to create. If null will use `$template` + * @return string|false Generated file content. + */ + public function bake($template, $content = '', $outputFile = null) + { + if ($outputFile === null) { + $outputFile = $template; + } + if ($content === true) { + $content = $this->getContent($template); + } + if (empty($content)) { + $this->err("No generated content for '{$template}.ctp', not generating template."); + + return false; + } + $this->out("\n" . sprintf('Baking `%s` view template file...', $outputFile), 1, Shell::QUIET); + $path = $this->getPath(); + $filename = $path . Inflector::underscore($outputFile) . '.ctp'; + $this->createFile($filename, $content); + + return $content; + } + + /** + * Builds content from template and variables + * + * @param string $action name to generate content to + * @param array|null $vars passed for use in templates + * @return string|false Content from template + */ + public function getContent($action, $vars = null) + { + if (!$vars) { + $vars = $this->_loadController(); + } + + if (empty($vars['primaryKey'])) { + $this->error('Cannot generate views for models with no primary key'); + + return false; + } + + if ($action === "index" && !empty($this->params['index-columns'])) { + $this->BakeTemplate->set('indexColumns', $this->params['index-columns']); + } + + $this->BakeTemplate->set('action', $action); + $this->BakeTemplate->set('plugin', $this->plugin); + $this->BakeTemplate->set($vars); + + return $this->BakeTemplate->generate("Template/$action"); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->setDescription( + 'Bake views for a controller, using built-in or custom templates. ' + )->addArgument('controller', [ + 'help' => 'Name of the controller views to bake. You can use Plugin.name as a shortcut for plugin baking.' + ])->addArgument('action', [ + 'help' => "Will bake a single action's file. core templates are (index, add, edit, view)" + ])->addArgument('alias', [ + 'help' => 'Will bake the template in but create the filename after .' + ])->addOption('controller', [ + 'help' => 'The controller name if you have a controller that does not follow conventions.' + ])->addOption('prefix', [ + 'help' => 'The routing prefix to generate views for.', + ])->addOption('index-columns', [ + 'help' => 'Limit for the number of index columns', + 'default' => 0 + ])->addSubcommand('all', [ + 'help' => '[optional] Bake all CRUD action views for all controllers. Requires models and controllers to exist.' + ]); + + return $parser; + } + + /** + * Get filtered associations + * To be mocked... + * + * @param \Cake\ORM\Table $model Table + * @return array associations + */ + protected function _filteredAssociations(Table $model) + { + if (is_null($this->_associationFilter)) { + $this->_associationFilter = new AssociationFilter(); + } + + return $this->_associationFilter->filterAssociations($model); + } +} diff --git a/app/vendor/cakephp/bake/src/Shell/Task/TestTask.php b/app/vendor/cakephp/bake/src/Shell/Task/TestTask.php new file mode 100644 index 000000000..939097cc5 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Shell/Task/TestTask.php @@ -0,0 +1,739 @@ + 'Model\Entity', + 'Table' => 'Model\Table', + 'Controller' => 'Controller', + 'Component' => 'Controller\Component', + 'Behavior' => 'Model\Behavior', + 'Helper' => 'View\Helper', + 'Shell' => 'Shell', + 'Task' => 'Shell\Task', + 'Shell_helper' => 'Shell\Helper', + 'Cell' => 'View\Cell', + 'Form' => 'Form', + 'Mailer' => 'Mailer', + ]; + + /** + * class types that methods can be generated for + * + * @var array + */ + public $classSuffixes = [ + 'entity' => '', + 'table' => 'Table', + 'controller' => 'Controller', + 'component' => 'Component', + 'behavior' => 'Behavior', + 'helper' => 'Helper', + 'shell' => 'Shell', + 'task' => 'Task', + 'shell_helper' => 'Helper', + 'cell' => 'Cell', + 'form' => 'Form', + 'mailer' => 'Mailer', + ]; + + /** + * Internal list of fixtures that have been added so far. + * + * @var array + */ + protected $_fixtures = []; + + /** + * Execution method always used for tasks + * + * @param string|null $type Class type. + * @param string|null $name Name. + * @return array|null + */ + public function main($type = null, $name = null) + { + parent::main(); + if (empty($type) && empty($name)) { + $this->outputTypeChoices(); + + return null; + } + + if ($this->param('all')) { + $this->_bakeAll($type); + + return null; + } + + if (empty($name)) { + return $this->outputClassChoices($type); + } + + if ($this->bake($type, $name)) { + $this->out('Done'); + } + } + + /** + * Output a list of class types you can bake a test for. + * + * @return void + */ + public function outputTypeChoices() + { + $this->out( + 'You must provide a class type to bake a test for. The valid types are:', + 2 + ); + $i = 0; + foreach ($this->classTypes as $option => $package) { + $this->out(++$i . '. ' . $option); + } + $this->out(''); + $this->out('Re-run your command as `cake bake `'); + } + + /** + * Output a list of possible classnames you might want to generate a test for. + * + * @param string $typeName The typename to get classes for. + * @return array + */ + public function outputClassChoices($typeName) + { + $type = $this->mapType($typeName); + $this->out( + 'You must provide a class to bake a test for. Some possible options are:', + 2 + ); + $options = $this->_getClassOptions($type); + $i = 0; + foreach ($options as $option) { + $this->out(++$i . '. ' . $option); + } + $this->out(''); + $this->out('Re-run your command as `cake bake ' . $typeName . ' `'); + + return $options; + } + + /** + * @param string $type The typename to get bake all classes for. + * @return void + */ + protected function _bakeAll($type) + { + $mappedType = $this->mapType($type); + $classes = $this->_getClassOptions($mappedType); + + foreach ($classes as $class) { + if ($this->bake($type, $class)) { + $this->out('Done - ' . $class . ''); + } else { + $this->out('Failed - ' . $class . ''); + } + } + + $this->out('Bake finished'); + } + + /** + * Get the possible classes for a given type. + * + * @param string $namespace The namespace fragment to look for classes in. + * @return array + */ + protected function _getClassOptions($namespace) + { + $classes = []; + $base = APP; + if ($this->plugin) { + $base = Plugin::classPath($this->plugin); + } + $path = $base . str_replace('\\', DS, $namespace); + $folder = new Folder($path); + list(, $files) = $folder->read(); + foreach ($files as $file) { + $classes[] = str_replace('.php', '', $file); + } + + return $classes; + } + + /** + * Completes final steps for generating data to create test case. + * + * @param string $type Type of object to bake test case for ie. Model, Controller + * @param string $className the 'cake name' for the class ie. Posts for the PostsController + * @return string|bool + */ + public function bake($type, $className) + { + if (!isset($this->classSuffixes[strtolower($type)]) || !isset($this->classTypes[ucfirst($type)])) { + return false; + } + + $fullClassName = $this->getRealClassName($type, $className); + + if (empty($this->params['no-fixture'])) { + if (!empty($this->params['fixtures'])) { + $fixtures = array_map('trim', explode(',', $this->params['fixtures'])); + $this->_fixtures = array_filter($fixtures); + } elseif ($this->typeCanDetectFixtures($type) && class_exists($fullClassName)) { + $this->out('Bake is detecting possible fixtures...'); + $testSubject = $this->buildTestSubject($type, $fullClassName); + $this->generateFixtureList($testSubject); + } + } + + $methods = []; + if (class_exists($fullClassName)) { + $methods = $this->getTestableMethods($fullClassName); + } + $mock = $this->hasMockClass($type); + list($preConstruct, $construction, $postConstruct) = $this->generateConstructor($type, $fullClassName); + $uses = $this->generateUses($type, $fullClassName); + + $subject = $className; + list($namespace, $className) = namespaceSplit($fullClassName); + + $baseNamespace = Configure::read('App.namespace'); + if ($this->plugin) { + $baseNamespace = $this->_pluginNamespace($this->plugin); + } + $subNamespace = substr($namespace, strlen($baseNamespace) + 1); + + $properties = $this->generateProperties($type, $subject, $fullClassName); + + $this->out("\n" . sprintf('Baking test case for %s ...', $fullClassName), 1, Shell::QUIET); + + $this->BakeTemplate->set('fixtures', $this->_fixtures); + $this->BakeTemplate->set('plugin', $this->plugin); + $this->BakeTemplate->set(compact( + 'subject', + 'className', + 'properties', + 'methods', + 'type', + 'fullClassName', + 'mock', + 'type', + 'preConstruct', + 'postConstruct', + 'construction', + 'uses', + 'baseNamespace', + 'subNamespace', + 'namespace' + )); + $out = $this->BakeTemplate->generate('tests/test_case'); + + $filename = $this->testCaseFileName($type, $fullClassName); + $emptyFile = $this->getPath() . $this->getSubspacePath($type) . DS . 'empty'; + $this->_deleteEmptyFile($emptyFile); + if ($this->createFile($filename, $out)) { + return $out; + } + + return false; + } + + /** + * Checks whether the chosen type can find its own fixtures. + * Currently only model, and controller are supported + * + * @param string $type The Type of object you are generating tests for eg. controller + * @return bool + */ + public function typeCanDetectFixtures($type) + { + $type = strtolower($type); + + return in_array($type, ['controller', 'table']); + } + + /** + * Construct an instance of the class to be tested. + * So that fixtures can be detected + * + * @param string $type The type of object you are generating tests for eg. controller + * @param string $class The classname of the class the test is being generated for. + * @return object And instance of the class that is going to be tested. + */ + public function buildTestSubject($type, $class) + { + if (strtolower($type) === 'table') { + list(, $name) = namespaceSplit($class); + $name = str_replace('Table', '', $name); + if ($this->plugin) { + $name = $this->plugin . '.' . $name; + } + if (TableRegistry::exists($name)) { + $instance = TableRegistry::get($name); + } else { + $instance = TableRegistry::get($name, [ + 'connectionName' => $this->connection + ]); + } + } elseif (strtolower($type) === 'controller') { + $instance = new $class(new Request(), new Response()); + } else { + $instance = new $class(); + } + + return $instance; + } + + /** + * Gets the real class name from the cake short form. If the class name is already + * suffixed with the type, the type will not be duplicated. + * + * @param string $type The Type of object you are generating tests for eg. controller. + * @param string $class the Classname of the class the test is being generated for. + * @return string Real class name + */ + public function getRealClassName($type, $class) + { + $namespace = Configure::read('App.namespace'); + if ($this->plugin) { + $namespace = str_replace('/', '\\', $this->plugin); + } + $suffix = $this->classSuffixes[strtolower($type)]; + $subSpace = $this->mapType($type); + if ($suffix && strpos($class, $suffix) === false) { + $class .= $suffix; + } + $prefix = $this->_getPrefix(); + if (in_array(strtolower($type), ['controller', 'cell']) && $prefix) { + $subSpace .= '\\' . str_replace('/', '\\', $prefix); + } + + return $namespace . '\\' . $subSpace . '\\' . $class; + } + + /** + * Gets the subspace path for a test. + * + * @param string $type The Type of object you are generating tests for eg. controller. + * @return string Path of the subspace. + */ + public function getSubspacePath($type) + { + $subspace = $this->mapType($type); + + return str_replace('\\', DS, $subspace); + } + + /** + * Map the types that TestTask uses to concrete types that App::className can use. + * + * @param string $type The type of thing having a test generated. + * @return string + * @throws \Cake\Core\Exception\Exception When invalid object types are requested. + */ + public function mapType($type) + { + $type = ucfirst($type); + if (empty($this->classTypes[$type])) { + throw new Exception('Invalid object type.'); + } + + return $this->classTypes[$type]; + } + + /** + * Get methods declared in the class given. + * No parent methods will be returned + * + * @param string $className Name of class to look at. + * @return array Array of method names. + */ + public function getTestableMethods($className) + { + $class = new ReflectionClass($className); + $out = []; + foreach ($class->getMethods() as $method) { + if ($method->getDeclaringClass()->getName() !== $className) { + continue; + } + if (!$method->isPublic()) { + continue; + } + $out[] = $method->getName(); + } + + return $out; + } + + /** + * Generate the list of fixtures that will be required to run this test based on + * loaded models. + * + * @param object $subject The object you want to generate fixtures for. + * @return array Array of fixtures to be included in the test. + */ + public function generateFixtureList($subject) + { + $this->_fixtures = []; + if ($subject instanceof Table) { + $this->_processModel($subject); + } elseif ($subject instanceof Controller) { + $this->_processController($subject); + } + + return array_values($this->_fixtures); + } + + /** + * Process a model recursively and pull out all the + * model names converting them to fixture names. + * + * @param \Cake\ORM\Table $subject A Model class to scan for associations and pull fixtures off of. + * @return void + */ + protected function _processModel($subject) + { + if (!$subject instanceof Table) { + return; + } + $this->_addFixture($subject->getAlias()); + foreach ($subject->associations()->keys() as $alias) { + $assoc = $subject->association($alias); + $target = $assoc->getTarget(); + $name = $target->getAlias(); + $subjectClass = get_class($subject); + + if ($subjectClass !== 'Cake\ORM\Table' && $subjectClass === get_class($target)) { + continue; + } + + if (!isset($this->_fixtures[$name])) { + $this->_processModel($target); + } + if ($assoc->type() === Association::MANY_TO_MANY) { + $junction = $assoc->junction(); + if (!isset($this->_fixtures[$junction->getAlias()])) { + $this->_processModel($junction); + } + } + } + } + + /** + * Process all the models attached to a controller + * and generate a fixture list. + * + * @param \Cake\Controller\Controller $subject A controller to pull model names off of. + * @return void + */ + protected function _processController($subject) + { + $models = [$subject->modelClass]; + foreach ($models as $model) { + list(, $model) = pluginSplit($model); + $this->_processModel($subject->{$model}); + } + } + + /** + * Add class name to the fixture list. + * Sets the app. or plugin.plugin_name. prefix. + * + * @param string $name Name of the Model class that a fixture might be required for. + * @return void + */ + protected function _addFixture($name) + { + if ($this->plugin) { + $prefix = 'plugin.' . Inflector::underscore($this->plugin) . '.'; + } else { + $prefix = 'app.'; + } + $fixture = $prefix . $this->_fixtureName($name); + $this->_fixtures[$name] = $fixture; + } + + /** + * Is a mock class required for this type of test? + * Controllers require a mock class. + * + * @param string $type The type of object tests are being generated for eg. controller. + * @return bool + */ + public function hasMockClass($type) + { + $type = strtolower($type); + + return $type === 'controller'; + } + + /** + * Generate a constructor code snippet for the type and class name + * + * @param string $type The Type of object you are generating tests for eg. controller + * @param string $fullClassName The full classname of the class the test is being generated for. + * @return array Constructor snippets for the thing you are building. + */ + public function generateConstructor($type, $fullClassName) + { + list(, $className) = namespaceSplit($fullClassName); + $type = strtolower($type); + $pre = $construct = $post = ''; + if ($type === 'table') { + $tableName = str_replace('Table', '', $className); + $pre = "\$config = TableRegistry::exists('{$tableName}') ? [] : ['className' => {$className}::class];"; + $construct = "TableRegistry::get('{$tableName}', \$config);"; + } + if ($type === 'behavior' || $type === 'entity' || $type === 'form') { + $construct = "new {$className}();"; + } + if ($type === 'helper') { + $pre = "\$view = new View();"; + $construct = "new {$className}(\$view);"; + } + if ($type === 'component') { + $pre = "\$registry = new ComponentRegistry();"; + $construct = "new {$className}(\$registry);"; + } + if ($type === 'shell') { + $pre = "\$this->io = \$this->getMockBuilder('Cake\Console\ConsoleIo')->getMock();"; + $construct = "new {$className}(\$this->io);"; + } + if ($type === 'task') { + $pre = "\$this->io = \$this->getMockBuilder('Cake\Console\ConsoleIo')->getMock();\n"; + $construct = "\$this->getMockBuilder('{$fullClassName}')\n"; + $construct .= " ->setConstructorArgs([\$this->io])\n"; + $construct .= " ->getMock();"; + } + if ($type === 'cell') { + $pre = "\$this->request = \$this->getMockBuilder('Cake\Http\ServerRequest')->getMock();\n"; + $pre .= " \$this->response = \$this->getMockBuilder('Cake\Http\Response')->getMock();"; + $construct = "new {$className}(\$this->request, \$this->response);"; + } + if ($type === 'shell_helper') { + $pre = "\$this->stub = new ConsoleOutput();\n"; + $pre .= " \$this->io = new ConsoleIo(\$this->stub);"; + $construct = "new {$className}(\$this->io);"; + } + + return [$pre, $construct, $post]; + } + + /** + * Generate property info for the type and class name + * + * The generated property info consists of a set of arrays that hold the following keys: + * + * - `description` (the property description) + * - `type` (the property docblock type) + * - `name` (the property name) + * - `value` (optional - the properties initial value) + * + * @param string $type The Type of object you are generating tests for eg. controller + * @param string $subject The name of the test subject. + * @param string $fullClassName The Classname of the class the test is being generated for. + * @return array An array containing property info + */ + public function generateProperties($type, $subject, $fullClassName) + { + $type = strtolower($type); + + $properties = []; + switch (strtolower($type)) { + case 'cell': + $properties[] = [ + 'description' => 'Request mock', + 'type' => '\Cake\Http\ServerRequest|\PHPUnit_Framework_MockObject_MockObject', + 'name' => 'request' + ]; + $properties[] = [ + 'description' => 'Response mock', + 'type' => '\Cake\Http\Response|\PHPUnit_Framework_MockObject_MockObject', + 'name' => 'response' + ]; + break; + + case 'shell': + case 'task': + $properties[] = [ + 'description' => 'ConsoleIo mock', + 'type' => '\Cake\Console\ConsoleIo|\PHPUnit_Framework_MockObject_MockObject', + 'name' => 'io' + ]; + break; + + case 'shell_helper': + $properties[] = [ + 'description' => 'ConsoleOutput stub', + 'type' => '\Cake\TestSuite\Stub\ConsoleOutput', + 'name' => 'stub' + ]; + $properties[] = [ + 'description' => 'ConsoleIo mock', + 'type' => '\Cake\Console\ConsoleIo', + 'name' => 'io' + ]; + break; + } + + if ($type !== 'controller') { + $properties[] = [ + 'description' => 'Test subject', + 'type' => '\\' . $fullClassName, + 'name' => $subject + ]; + } + + return $properties; + } + + /** + * Generate the uses() calls for a type & class name + * + * @param string $type The Type of object you are generating tests for eg. controller + * @param string $fullClassName The Classname of the class the test is being generated for. + * @return array An array containing used classes + */ + public function generateUses($type, $fullClassName) + { + $uses = []; + $type = strtolower($type); + if ($type === 'component') { + $uses[] = 'Cake\Controller\ComponentRegistry'; + } + if ($type === 'table') { + $uses[] = 'Cake\ORM\TableRegistry'; + } + if ($type === 'helper') { + $uses[] = 'Cake\View\View'; + } + if ($type === 'shell_helper') { + $uses[] = 'Cake\TestSuite\Stub\ConsoleOutput'; + $uses[] = 'Cake\Console\ConsoleIo'; + } + $uses[] = $fullClassName; + + return $uses; + } + + /** + * Get the file path. + * + * @return string + */ + public function getPath() + { + $dir = 'TestCase/'; + $path = defined('TESTS') ? TESTS . $dir : ROOT . DS . 'tests' . DS . $dir; + if (isset($this->plugin)) { + $path = $this->_pluginPath($this->plugin) . 'tests/' . $dir; + } + + return $path; + } + + /** + * Make the filename for the test case. resolve the suffixes for controllers + * and get the plugin path if needed. + * + * @param string $type The Type of object you are generating tests for eg. controller + * @param string $className The fully qualified classname of the class the test is being generated for. + * @return string filename the test should be created on. + */ + public function testCaseFileName($type, $className) + { + $path = $this->getPath(); + $namespace = Configure::read('App.namespace'); + if ($this->plugin) { + $namespace = $this->plugin; + } + + $classTail = substr($className, strlen($namespace) + 1); + $path = $path . $classTail . 'Test.php'; + + return str_replace(['/', '\\'], DS, $path); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $types = array_keys($this->classTypes); + $types = array_merge($types, array_map('strtolower', $types)); + + $parser->setDescription( + 'Bake test case skeletons for classes.' + )->addArgument('type', [ + 'help' => 'Type of class to bake, can be any of the following:' . + ' controller, model, helper, component or behavior.', + 'choices' => $types, + ])->addArgument('name', [ + 'help' => 'An existing class to bake tests for.' + ])->addOption('fixtures', [ + 'help' => 'A comma separated list of fixture names you want to include.' + ])->addOption('no-fixture', [ + 'boolean' => true, + 'default' => false, + 'help' => 'Select if you want to bake without fixture.' + ])->addOption('prefix', [ + 'default' => false, + 'help' => 'Use when baking tests for prefixed controllers.' + ])->addOption('all', [ + 'boolean' => true, + 'help' => 'Bake all classes of the given type' + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Controller/component.twig b/app/vendor/cakephp/bake/src/Template/Bake/Controller/component.twig new file mode 100644 index 000000000..65ba0b97f --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Controller/component.twig @@ -0,0 +1,34 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} +{{ currentModelName }}->newEntity(); + if ($this->request->is('post')) { + ${{ singularName }} = $this->{{ currentModelName }}->patchEntity(${{ singularName }}, $this->request->getData()); + if ($this->{{ currentModelName }}->save(${{ singularName }})) { + $this->Flash->success(__('The {{ singularHumanName|lower }} has been saved.')); + + return $this->redirect(['action' => 'index']); + } + $this->Flash->error(__('The {{ singularHumanName|lower }} could not be saved. Please, try again.')); + } +{% set associations = Bake.aliasExtractor(modelObj, 'BelongsTo') %} +{% set associations = associations|merge(Bake.aliasExtractor(modelObj, 'BelongsToMany')) %} + +{%- for assoc in associations %} + {%- set otherName = Bake.getAssociatedTableAlias(modelObj, assoc) %} + {%- set otherPlural = otherName|variable %} + ${{ otherPlural }} = $this->{{ currentModelName }}->{{ otherName }}->find('list', ['limit' => 200]); + {{- "\n" }} + {%- set compact = compact|merge(["'#{otherPlural}'"]) %} +{% endfor %} + $this->set(compact({{ compact|join(', ')|raw }})); + } diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/delete.twig b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/delete.twig new file mode 100644 index 000000000..2fbbf6db1 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/delete.twig @@ -0,0 +1,35 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} + + /** + * Delete method + * + * @param string|null $id {{ singularHumanName }} id. + * @return \Cake\Http\Response|null Redirects to index. + * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. + */ + public function delete($id = null) + { + $this->request->allowMethod(['post', 'delete']); + ${{ singularName }} = $this->{{ currentModelName }}->get($id); + if ($this->{{ currentModelName }}->delete(${{ singularName }})) { + $this->Flash->success(__('The {{ singularHumanName|lower }} has been deleted.')); + } else { + $this->Flash->error(__('The {{ singularHumanName|lower }} could not be deleted. Please, try again.')); + } + + return $this->redirect(['action' => 'index']); + } diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/edit.twig b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/edit.twig new file mode 100644 index 000000000..d06cb9c77 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/edit.twig @@ -0,0 +1,49 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} +{% set belongsTo = Bake.aliasExtractor(modelObj, 'BelongsTo') %} +{% set belongsToMany = Bake.aliasExtractor(modelObj, 'belongsToMany') %} +{% set compact = ["'#{singularName}'"] %} + + /** + * Edit method + * + * @param string|null $id {{ singularHumanName }} id. + * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise. + * @throws \Cake\Network\Exception\NotFoundException When record not found. + */ + public function edit($id = null) + { + ${{ singularName }} = $this->{{ currentModelName }}->get($id, [ + 'contain' => [{{ Bake.stringifyList(belongsToMany, {'indent': false})|raw }}] + ]); + if ($this->request->is(['patch', 'post', 'put'])) { + ${{ singularName }} = $this->{{ currentModelName }}->patchEntity(${{ singularName }}, $this->request->getData()); + if ($this->{{ currentModelName }}->save(${{ singularName }})) { + $this->Flash->success(__('The {{ singularHumanName|lower }} has been saved.')); + + return $this->redirect(['action' => 'index']); + } + $this->Flash->error(__('The {{ singularHumanName|lower }} could not be saved. Please, try again.')); + } +{% for assoc in belongsTo|merge(belongsToMany) %} + {%- set otherName = Bake.getAssociatedTableAlias(modelObj, assoc) %} + {%- set otherPlural = otherName|variable %} + ${{ otherPlural }} = $this->{{ currentModelName }}->{{ otherName }}->find('list', ['limit' => 200]); + {{- "\n" }} + {%- set compact = compact|merge(["'#{otherPlural}'"]) %} +{% endfor %} + $this->set(compact({{ compact|join(', ')|raw }})); + } diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/index.twig b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/index.twig new file mode 100644 index 000000000..181e830b4 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/index.twig @@ -0,0 +1,33 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} + + /** + * Index method + * + * @return \Cake\Http\Response|void + */ + public function index() + { +{% set belongsTo = Bake.aliasExtractor(modelObj, 'BelongsTo') %} +{% if belongsTo %} + $this->paginate = [ + 'contain' => [{{ Bake.stringifyList(belongsTo, {'indent': false})|raw }}] + ]; +{% endif %} + ${{ pluralName }} = $this->paginate($this->{{ currentModelName }}); + + $this->set(compact('{{ pluralName }}')); + } diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/login.twig b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/login.twig new file mode 100644 index 000000000..ee473edad --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/login.twig @@ -0,0 +1,33 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} + + /** + * Login method + * + * @return \Cake\Http\Response|null + */ + public function login() + { + if ($this->request->is('post')) { + $user = $this->Auth->identify(); + if ($user) { + $this->Auth->setUser($user); + + return $this->redirect($this->Auth->redirectUrl()); + } + $this->Flash->error(__('Invalid credentials, try again')); + } + } diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/logout.twig b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/logout.twig new file mode 100644 index 000000000..facbd2fe6 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/logout.twig @@ -0,0 +1,25 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} + + /** + * Logout method + * + * @return \Cake\Http\Response + */ + public function logout() + { + return $this->redirect($this->Auth->logout()); + } diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/view.twig b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/view.twig new file mode 100644 index 000000000..7091ba4a1 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Element/Controller/view.twig @@ -0,0 +1,35 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} +{% set allAssociations = Bake.aliasExtractor(modelObj, 'BelongsTo') %} +{% set allAssociations = allAssociations|merge(Bake.aliasExtractor(modelObj, 'BelongsToMany')) %} +{% set allAssociations = allAssociations|merge(Bake.aliasExtractor(modelObj, 'HasOne')) %} +{% set allAssociations = allAssociations|merge(Bake.aliasExtractor(modelObj, 'HasMany')) %} + + /** + * View method + * + * @param string|null $id {{ singularHumanName }} id. + * @return \Cake\Http\Response|void + * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. + */ + public function view($id = null) + { + ${{ singularName }} = $this->{{ currentModelName }}->get($id, [ + 'contain' => [{{ Bake.stringifyList(allAssociations, {'indent': false})|raw }}] + ]); + + $this->set('{{ singularName }}', ${{ singularName }}); + } diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Element/array_property.twig b/app/vendor/cakephp/bake/src/Template/Bake/Element/array_property.twig new file mode 100644 index 000000000..d177d81a6 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Element/array_property.twig @@ -0,0 +1,7 @@ + + /** + * {{ name|humanize }} + * + * @var array + */ + public ${{ name }} = [{{ Bake.stringifyList(value, {'indent': false})|raw }}]; \ No newline at end of file diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Element/form.twig b/app/vendor/cakephp/bake/src/Template/Bake/Element/form.twig new file mode 100644 index 000000000..b2d9e5005 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Element/form.twig @@ -0,0 +1,80 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} +{% set fields = Bake.filterFields(fields, schema, modelObject) %} + +
+ Form->create(${{ singularVar }}) ?> +
+ + Form->control('{{ field }}', ['options' => ${{ keyFields[field] }}, 'empty' => true]); + {{- "\n" }} + {%- else %} + echo $this->Form->control('{{ field }}', ['options' => ${{ keyFields[field] }}]); + {{- "\n" }} + {%- endif %} + {%- elseif field not in ['created', 'modified', 'updated'] %} + {%- set fieldData = Bake.columnData(field, schema) %} + {%- if fieldData.type in ['date', 'datetime', 'time'] and fieldData.null %} + echo $this->Form->control('{{ field }}', ['empty' => true]); + {{- "\n" }} + {%- else %} + echo $this->Form->control('{{ field }}'); + {{- "\n" }} + {%- endif %} + {%- endif %} +{%- endfor %} + +{%- if associations.BelongsToMany %} + {%- for assocName, assocData in associations.BelongsToMany %} + echo $this->Form->control('{{ assocData.property }}._ids', ['options' => ${{ assocData.variable }}]); + {{- "\n" }} + {%- endfor %} +{% endif %} + ?> +
+ Form->button(__('Submit')) ?> + Form->end() ?> +
diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Form/form.twig b/app/vendor/cakephp/bake/src/Template/Bake/Form/form.twig new file mode 100644 index 000000000..835c42c6e --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Form/form.twig @@ -0,0 +1,60 @@ +{# +/** +* CakePHP(tm) : Rapid Development Framework (http://cakephp.org) +* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) +* +* Licensed under The MIT License +* For full copyright and license information, please see the LICENSE.txt +* Redistributions of files must retain the above copyright notice. +* +* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) +* @link http://cakephp.org CakePHP(tm) Project +* @since 2.0.0 +* @license http://www.opensource.org/licenses/mit-license.php MIT License +*/ +#} +setTable('{{ table }}'); +{% endif %} + +{%- if displayField %} + $this->setDisplayField('{{ displayField }}'); +{% endif %} + +{%- if primaryKey %} + {%- if primaryKey is iterable and primaryKey|length > 1 %} + $this->setPrimaryKey([{{ Bake.stringifyList(primaryKey, {'indent': false})|raw }}]); + {{- "\n" }} + {%- else %} + $this->setPrimaryKey('{{ primaryKey|as_array|first }}'); + {{- "\n" }} + {%- endif %} +{% endif %} + +{%- if behaviors %} + +{% endif %} + +{%- for behavior, behaviorData in behaviors %} + $this->addBehavior('{{ behavior }}'{{ (behaviorData ? (", [" ~ Bake.stringifyList(behaviorData, {'indent': 3, 'quotes': false})|raw ~ ']') : '')|raw }}); +{% endfor %} + +{%- if associations.belongsTo or associations.hasMany or associations.belongsToMany %} + +{% endif %} + +{%- for type, assocs in associations %} + {%- for assoc in assocs %} + {%- set assocData = [] %} + {%- for key, val in assoc if key is not same as('alias') %} + {%- set assocData = assocData|merge({(key): val}) %} + {%- endfor %} + $this->{{ type }}('{{ assoc.alias }}', [{{ Bake.stringifyList(assocData, {'indent': 3})|raw }}]); + {{- "\n" }} + {%- endfor %} +{% endfor %} + } +{{- "\n" }} + +{%- if validation %} + + /** + * Default validation rules. + * + * @param \Cake\Validation\Validator $validator Validator instance. + * @return \Cake\Validation\Validator + */ + public function validationDefault(Validator $validator) + { +{% for field, rules in validation %} +{% set validationMethods = Bake.getValidationMethods(field, rules) %} +{% if validationMethods %} + $validator +{% for validationMethod in validationMethods %} +{% if loop.last %} +{% set validationMethod = validationMethod ~ ';' %} +{% endif %} + {{ validationMethod|raw }} +{% endfor %} + +{% endif %} +{% endfor %} + return $validator; + } +{% endif %} + +{%- if rulesChecker %} + + /** + * Returns a rules checker object that will be used for validating + * application integrity. + * + * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. + * @return \Cake\ORM\RulesChecker + */ + public function buildRules(RulesChecker $rules) + { +{% for field, rule in rulesChecker %} + $rules->add($rules->{{ rule.name }}(['{{ field }}']{{ (rule.extra ? (", '#{rule.extra}'") : '')|raw }})); +{% endfor %} + + return $rules; + } +{% endif %} + +{%- if connection is not same as('default') %} + + /** + * Returns the database connection name to use by default. + * + * @return string + */ + public static function defaultConnectionName() + { + return '{{ connection }}'; + } +{% endif %} +} diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Plugin/.gitignore.twig b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/.gitignore.twig new file mode 100644 index 000000000..bc959c533 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/.gitignore.twig @@ -0,0 +1,3 @@ +/composer.lock +/phpunit.xml +/vendor diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Plugin/README.md.twig b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/README.md.twig new file mode 100644 index 000000000..8ede8ffc0 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/README.md.twig @@ -0,0 +1,26 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} +# {{ plugin }} plugin for CakePHP + +## Installation + +You can install this plugin into your CakePHP application using [composer](http://getcomposer.org). + +The recommended way to install composer packages is: + +``` +composer require {{ package }} +``` diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Plugin/composer.json.twig b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/composer.json.twig new file mode 100644 index 000000000..a573d8394 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/composer.json.twig @@ -0,0 +1,39 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} +{% set namespace = namespace|replace({'\\': '\\\\'}) %} +{ + "name": "{{ package }}", + "description": "{{ plugin }} plugin for CakePHP", + "type": "cakephp-plugin", + "license": "MIT", + "require": { + "cakephp/cakephp": "^3.4" + }, + "require-dev": { + "phpunit/phpunit": "^5.7|^6.0" + }, + "autoload": { + "psr-4": { + "{{ namespace }}\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "{{ namespace }}\\Test\\": "tests", + "Cake\\Test\\": "./vendor/cakephp/cakephp/tests" + } + } +} diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Plugin/config/routes.php.twig b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/config/routes.php.twig new file mode 100644 index 000000000..30c802010 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/config/routes.php.twig @@ -0,0 +1,27 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} + '/{{ routePath }}'], + function (RouteBuilder $routes) { + $routes->fallbacks(DashedRoute::class); + } +); diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Plugin/phpunit.xml.dist.twig b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/phpunit.xml.dist.twig new file mode 100644 index 000000000..bd3feb5a5 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/phpunit.xml.dist.twig @@ -0,0 +1,53 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} + + + + + + + + + + + ./tests/TestCase + + + + + + + + + + + + + + + ./src/ + + + + diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Plugin/src/Controller/AppController.php.twig b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/src/Controller/AppController.php.twig new file mode 100644 index 000000000..d06ea6823 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Plugin/src/Controller/AppController.php.twig @@ -0,0 +1,25 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} +out($this->OptionParser->help()); + } +} diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Shell/task.twig b/app/vendor/cakephp/bake/src/Template/Bake/Shell/task.twig new file mode 100644 index 000000000..7000f5ddd --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Shell/task.twig @@ -0,0 +1,47 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} + +{% element 'form' %} diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Template/edit.twig b/app/vendor/cakephp/bake/src/Template/Bake/Template/edit.twig new file mode 100644 index 000000000..cde0d3567 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Template/edit.twig @@ -0,0 +1,22 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} + +{% element 'form' %} diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Template/index.twig b/app/vendor/cakephp/bake/src/Template/Bake/Template/index.twig new file mode 100644 index 000000000..79050ef67 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Template/index.twig @@ -0,0 +1,90 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} + +{% set fields = Bake.filterFields(fields, schema, modelObject, indexColumns, ['binary', 'text']) %} + +
+

+ + + +{% for field in fields %} + +{% endfor %} + + + + + + +{% for field in fields %} +{% set isKey = false %} +{% if associations.BelongsTo %} +{% for alias, details in associations.BelongsTo if field == details.foreignKey %} +{% set isKey = true %} + +{% endfor %} +{% endif %} +{% if isKey is not same as(true) %} +{% set columnData = Bake.columnData(field, schema) %} +{% if columnData.type not in ['integer', 'float', 'decimal', 'biginteger', 'smallinteger', 'tinyinteger'] %} + +{% else %} + +{% endif %} +{% endif %} +{% endfor %} +{% set pk = '$' ~ singularVar ~ '->' ~ primaryKey[0] %} + + + + +
Paginator->sort('{{ field }}') ?>
has('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>{{ field }}) ?>Number->format(${{ singularVar }}->{{ field }}) ?> + Html->link(__('View'), ['action' => 'view', {{ pk|raw }}]) ?> + Html->link(__('Edit'), ['action' => 'edit', {{ pk|raw }}]) ?> + Form->postLink(__('Delete'), ['action' => 'delete', {{ pk|raw }}], ['confirm' => __('Are you sure you want to delete # {0}?', {{ pk|raw }})]) ?> +
+
+
    + Paginator->first('<< ' . __('first')) ?> + Paginator->prev('< ' . __('previous')) ?> + Paginator->numbers() ?> + Paginator->next(__('next') . ' >') ?> + Paginator->last(__('last') . ' >>') ?> +
+

Paginator->counter(['format' => __('Page {{ '{{' }}page{{ '}}' }} of {{ '{{' }}pages{{ '}}' }}, showing {{ '{{' }}current{{ '}}' }} record(s) out of {{ '{{' }}count{{ '}}' }} total')]) ?>

+
+
diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Template/login.twig b/app/vendor/cakephp/bake/src/Template/Bake/Template/login.twig new file mode 100644 index 000000000..7b03c8cff --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Template/login.twig @@ -0,0 +1,31 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} + +
+Flash->render('auth') ?> + Form->create() ?> +
+ + Form->control('username') ?> + Form->control('password') ?> +
+ Form->button(__('Login')); ?> + Form->end() ?> +
diff --git a/app/vendor/cakephp/bake/src/Template/Bake/Template/view.twig b/app/vendor/cakephp/bake/src/Template/Bake/Template/view.twig new file mode 100644 index 000000000..200a2f869 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/Template/view.twig @@ -0,0 +1,137 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} + +{% set associations = {'BelongsTo': [], 'HasOne': [], 'HasMany': [], 'BelongsToMany': []}|merge(associations) %} +{% set fieldsData = Bake.getViewFieldsData(fields, schema, associations) %} +{% set associationFields = fieldsData.associationFields %} +{% set groupedFields = fieldsData.groupedFields %} +{% set pK = '$' ~ singularVar ~ '->' ~ primaryKey[0] %} + +
+

{{ displayField }}) ?>

+ +{% if groupedFields['string'] %} +{% for field in groupedFields['string'] %} +{% if associationFields[field] %} +{% set details = associationFields[field] %} + + + + +{% else %} + + + + +{% endif %} +{% endfor %} +{% endif %} +{% if associations.HasOne %} +{% for alias, details in associations.HasOne %} + + + + +{% endfor %} +{% endif %} +{% if groupedFields.number %} +{% for field in groupedFields.number %} + + + + +{% endfor %} +{% endif %} +{% if groupedFields.date %} +{% for field in groupedFields.date %} + + + + +{% endfor %} +{% endif %} +{% if groupedFields.boolean %} +{% for field in groupedFields.boolean %} + + + + +{% endfor %} +{% endif %} +
has('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>
{{ field }}) ?>
has('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>
Number->format(${{ singularVar }}->{{ field }}) ?>
{{ field }}) ?>
{{ field }} ? __('Yes') : __('No'); ?>
+{% if groupedFields.text %} +{% for field in groupedFields.text %} +
+

+ Text->autoParagraph(h(${{ singularVar }}->{{ field }})); ?> +
+{% endfor %} +{% endif %} +{% set relations = associations.BelongsToMany|merge(associations.HasMany) %} +{% for alias, details in relations %} +{% set otherSingularVar = alias|variable %} +{% set otherPluralHumanName = details.controller|underscore|humanize %} + +{% endfor %} +
diff --git a/app/vendor/cakephp/bake/src/Template/Bake/View/cell.twig b/app/vendor/cakephp/bake/src/Template/Bake/View/cell.twig new file mode 100644 index 000000000..0f9c335d3 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Template/Bake/View/cell.twig @@ -0,0 +1,43 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @since 2.0.0 + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +#} +{{ (subject ~ ' = ' ~ construction)|raw }} +{% if postConstruct %} + {{ postConstruct|raw }} +{% endif %} + } + + /** + * tearDown method + * + * @return void + */ + public function tearDown() + { + unset($this->{{ subject }}); + + parent::tearDown(); + } +{% endif %} + +{%- for method in methods %} + + /** + * Test {{ method }} method + * + * @return void + */ + public function test{{ method|camelize }}() + { + $this->markTestIncomplete('Not implemented yet.'); + } +{% endfor %} + +{%- if not methods %} + + /** + * Test initial setup + * + * @return void + */ + public function testInitialization() + { + $this->markTestIncomplete('Not implemented yet.'); + } +{% endif %} +} diff --git a/app/vendor/cakephp/bake/src/Utility/CommonOptionsTrait.php b/app/vendor/cakephp/bake/src/Utility/CommonOptionsTrait.php new file mode 100644 index 000000000..739d5ad15 --- /dev/null +++ b/app/vendor/cakephp/bake/src/Utility/CommonOptionsTrait.php @@ -0,0 +1,59 @@ +addOption('plugin', [ + 'short' => 'p', + 'help' => 'Plugin to bake into.' + ])->addOption('force', [ + 'short' => 'f', + 'boolean' => true, + 'help' => 'Force overwriting existing files without prompting.' + ])->addOption('connection', [ + 'short' => 'c', + 'default' => 'default', + 'help' => 'The datasource connection to get data from.' + ])->addOption('theme', [ + 'short' => 't', + 'help' => 'The theme to use when baking code.', + 'default' => Configure::read('Bake.theme'), + 'choices' => $bakeThemes + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/bake/src/Utility/Model/AssociationFilter.php b/app/vendor/cakephp/bake/src/Utility/Model/AssociationFilter.php new file mode 100644 index 000000000..af7e2038a --- /dev/null +++ b/app/vendor/cakephp/bake/src/Utility/Model/AssociationFilter.php @@ -0,0 +1,114 @@ +belongsToManyJunctionAliases($table); + + return array_values(array_diff($aliases, $belongsToManyJunctionsAliases)); + } + + /** + * Get the array of junction aliases for all the BelongsToMany associations + * + * @param Table $table Table + * @return array junction aliases of all the BelongsToMany associations + */ + public function belongsToManyJunctionAliases(Table $table) + { + $extractor = function ($val) { + return $val->junction()->getAlias(); + }; + + return array_map($extractor, $table->associations()->type('BelongsToMany')); + } + + /** + * Returns filtered associations for controllers models. HasMany association are filtered if + * already existing in BelongsToMany + * + * @param Table $model The model to build associations for. + * @return array associations + */ + public function filterAssociations(Table $model) + { + $belongsToManyJunctionsAliases = $this->belongsToManyJunctionAliases($model); + $keys = ['BelongsTo', 'HasOne', 'HasMany', 'BelongsToMany']; + $associations = []; + + foreach ($keys as $type) { + foreach ($model->associations()->type($type) as $assoc) { + $target = $assoc->getTarget(); + $assocName = $assoc->getName(); + $alias = $target->getAlias(); + //filter existing HasMany + if ($type === 'HasMany' && in_array($alias, $belongsToManyJunctionsAliases)) { + continue; + } + $targetClass = get_class($target); + list(, $className) = namespaceSplit($targetClass); + + $navLink = true; + $modelClass = get_class($model); + if ($modelClass !== Table::class && $targetClass === $modelClass) { + $navLink = false; + } + + $className = preg_replace('/(.*)Table$/', '\1', $className); + if ($className === '') { + $className = $alias; + } + + try { + $associations[$type][$assocName] = [ + 'property' => $assoc->getProperty(), + 'variable' => Inflector::variable($assocName), + 'primaryKey' => (array)$target->getPrimaryKey(), + 'displayField' => $target->getDisplayField(), + 'foreignKey' => $assoc->getForeignKey(), + 'alias' => $alias, + 'controller' => $className, + 'fields' => $target->getSchema()->columns(), + 'navLink' => $navLink, + ]; + } catch (Exception $e) { + // Do nothing it could be a bogus association name. + } + } + } + + return $associations; + } +} diff --git a/app/vendor/cakephp/bake/src/View/BakeView.php b/app/vendor/cakephp/bake/src/View/BakeView.php new file mode 100644 index 000000000..61cc096a3 --- /dev/null +++ b/app/vendor/cakephp/bake/src/View/BakeView.php @@ -0,0 +1,263 @@ + tags + * - Add an extra newline to <%=, to counteract php automatically removing a newline + * - Replace remaining <=% with php short echo tags + * - Replace <% with php open tags + * - Replace %> with php close tags + * + * Replacements that start with `/` will be treated as regex replacements. + * All other values will be treated used with str_replace() + * + * @var array + */ + protected $_defaultConfig = [ + 'phpTagReplacements' => [ + ' "' => "CakePHPBakeCloseTag>" + ], + 'replacements' => [ + '/\n[ \t]+<%-( |$)/' => "\n<% ", + '/-%>/' => "?>", + '/<%=(.*)\%>\n(.)/' => "<%=$1%>\n\n$2", + '<%=' => ' '' => '?>' + ] + ]; + + /** + * Path where bake's intermediary files are written. + * Defaults to `TMP . 'bake' . DS`. + * + * @var string + */ + protected $_tmpLocation; + + /** + * Templates extensions to search for. + * + * @var array + */ + protected $extensions = [ + '.twig', + '.ctp', + ]; + + /** + * View file being evaluated. + * + * @var string + */ + protected $__viewFile; + + /** + * Initialize view + * + * @return void + */ + public function initialize() + { + $bakeTemplates = dirname(dirname(__FILE__)) . DS . 'Template' . DS; + $paths = (array)Configure::read('App.paths.templates'); + + if (!in_array($bakeTemplates, $paths)) { + $paths[] = $bakeTemplates; + Configure::write('App.paths.templates', $paths); + } + + $this->_tmpLocation = TMP . 'bake' . DS; + if (!file_exists($this->_tmpLocation)) { + mkdir($this->_tmpLocation); + } + + parent::initialize(); + } + + /** + * Renders view for given view file and layout. + * + * Render triggers helper callbacks, which are fired before and after the view are rendered, + * as well as before and after the layout. The helper callbacks are called: + * + * - `beforeRender` + * - `afterRender` + * + * View names can point to plugin views/layouts. Using the `Plugin.view` syntax + * a plugin view/layout can be used instead of the app ones. If the chosen plugin is not found + * the view will be located along the regular view path cascade. + * + * View can also be a template string, rather than the name of a view file + * + * @param string|null $view Name of view file to use, or a template string to render + * @param string|null $layout Layout to use. Not used, for consistency with other views only + * @return string|null Rendered content. + * @throws \Cake\Core\Exception\Exception If there is an error in the view. + */ + public function render($view = null, $layout = null) + { + $viewFileName = $this->_getViewFileName($view); + $templateEventName = str_replace( + ['.ctp', '.twig', DS], + ['', '', '.'], + explode('Template' . DS . 'Bake' . DS, $viewFileName)[1] + ); + + $this->_currentType = static::TYPE_TEMPLATE; + $this->dispatchEvent('View.beforeRender', [$viewFileName]); + $this->dispatchEvent('View.beforeRender.' . $templateEventName, [$viewFileName]); + $this->Blocks->set('content', $this->_render($viewFileName)); + $this->dispatchEvent('View.afterRender', [$viewFileName]); + $this->dispatchEvent('View.afterRender.' . $templateEventName, [$viewFileName]); + + if ($layout === null) { + $layout = $this->layout; + } + if ($layout && $this->autoLayout) { + $this->Blocks->set('content', $this->renderLayout('', $layout)); + } + + return $this->Blocks->get('content'); + } + + /** + * Wrapper for creating and dispatching events. + * + * Use the Bake prefix for bake related view events + * + * @param string $name Name of the event. + * @param array|null $data Any value you wish to be transported with this event to + * it can be read by listeners. + * + * @param object|null $subject The object that this event applies to + * ($this by default). + * + * @return \Cake\Event\Event + */ + public function dispatchEvent($name, $data = null, $subject = null) + { + $name = preg_replace('/^View\./', 'Bake.', $name); + + return parent::dispatchEvent($name, $data, $subject); + } + + /** + * Sandbox method to evaluate a template / view script in. + * + * @param string $viewFile Filename of the view + * @param array $dataForView Data to include in rendered view. + * If empty the current View::$viewVars will be used. + * @return string Rendered output + */ + protected function _evaluate($viewFile, $dataForView) + { + if (substr($viewFile, -4) === 'twig') { + return parent::_evaluate($viewFile, $dataForView); + } + + $viewString = $this->_getViewFileContents($viewFile); + + $replacements = $this->getConfig('phpTagReplacements') + $this->getConfig('replacements'); + + foreach ($replacements as $find => $replace) { + if ($this->_isRegex($find)) { + $viewString = preg_replace($find, $replace, $viewString); + } else { + $viewString = str_replace($find, $replace, $viewString); + } + } + + $this->__viewFile = $this->_tmpLocation . Text::slug(preg_replace('@.*Template[/\\\\]@', '', $viewFile)) . '.php'; + file_put_contents($this->__viewFile, $viewString); + + unset($viewFile, $viewString, $replacements, $find, $replace); + extract($dataForView); + ob_start(); + + include $this->__viewFile; + + $content = ob_get_clean(); + + $unPhp = $this->getConfig('phpTagReplacements'); + + return str_replace(array_values($unPhp), array_keys($unPhp), $content); + } + + /** + * Get the contents of the template file + * + * @param string $filename A template filename + * @return string Bake template to evaluate + */ + protected function _getViewFileContents($filename) + { + return file_get_contents($filename); + } + + /** + * Return all possible paths to find view files in order + * + * @param string $plugin Optional plugin name to scan for view files. + * @param bool $cached Set to false to force a refresh of view paths. Default true. + * @return array paths + */ + protected function _paths($plugin = null, $cached = true) + { + $paths = parent::_paths($plugin, false); + foreach ($paths as &$path) { + $path .= 'Bake' . DS; + } + + return $paths; + } + + /** + * Check if a replacement pattern is a regex + * + * Use preg_match to detect invalid regexes + * + * @param string $maybeRegex a fixed string or a regex + * @return bool + */ + protected function _isRegex($maybeRegex) + { + return substr($maybeRegex, 0, 1) === '/'; + } +} diff --git a/app/vendor/cakephp/bake/src/View/Helper/BakeHelper.php b/app/vendor/cakephp/bake/src/View/Helper/BakeHelper.php new file mode 100644 index 000000000..7b47e25ef --- /dev/null +++ b/app/vendor/cakephp/bake/src/View/Helper/BakeHelper.php @@ -0,0 +1,430 @@ + $name, + 'value' => $value + ]; + + return $this->_View->element('array_property', $options); + } + + /** + * Returns an array converted into a formatted multiline string + * + * @param array $list array of items to be stringified + * @param array $options options to use + * @return string + */ + public function stringifyList(array $list, array $options = []) + { + $options += [ + 'indent' => 2, + 'tab' => ' ', + 'trailingComma' => false, + 'quotes' => true + ]; + + if (!$list) { + return ''; + } + + foreach ($list as $k => &$v) { + if ($options['quotes']) { + $v = "'$v'"; + } + if (!is_numeric($k)) { + $nestedOptions = $options; + if ($nestedOptions['indent']) { + $nestedOptions['indent'] += 1; + } + if (is_array($v)) { + $v = sprintf( + "'%s' => [%s]", + $k, + $this->stringifyList($v, $nestedOptions) + ); + } else { + $v = "'$k' => $v"; + } + } elseif (is_array($v)) { + $nestedOptions = $options; + if ($nestedOptions['indent']) { + $nestedOptions['indent'] += 1; + } + $v = sprintf( + "[%s]", + $this->stringifyList($v, $nestedOptions) + ); + } + } + + $start = $end = ''; + $join = ', '; + if ($options['indent']) { + $join = ','; + $start = "\n" . str_repeat($options['tab'], $options['indent']); + $join .= $start; + $end = "\n" . str_repeat($options['tab'], $options['indent'] - 1); + } + + if ($options['trailingComma']) { + $end = "," . $end; + } + + return $start . implode($join, $list) . $end; + } + + /** + * Extract the aliases for associations, filters hasMany associations already extracted as + * belongsToMany + * + * @param \Cake\ORM\Table $table object to find associations on + * @param string $assoc association to extract + * @return array + */ + public function aliasExtractor($table, $assoc) + { + $extractor = function ($val) { + return $val->getTarget()->getAlias(); + }; + $aliases = array_map($extractor, $table->associations()->type($assoc)); + if ($assoc === 'HasMany') { + return $this->_filterHasManyAssociationsAliases($table, $aliases); + } + + return $aliases; + } + + /** + * Returns details about the given class. + * + * The returned array holds the following keys: + * + * - `fqn` (the fully qualified name) + * - `namespace` (the full namespace without leading separator) + * - `class` (the class name) + * - `plugin` (either the name of the plugin, or `null`) + * - `name` (the name of the component without suffix) + * - `fullName` (the full name of the class, including possible vendor and plugin name) + * + * @param string $class Class name + * @param string $type Class type/sub-namespace + * @param string $suffix Class name suffix + * @return array Class info + */ + public function classInfo($class, $type, $suffix) + { + list($plugin, $name) = \pluginSplit($class); + + $base = Configure::read('App.namespace'); + if ($plugin !== null) { + $base = $plugin; + } + $base = str_replace('/', '\\', trim($base, '\\')); + $sub = '\\' . str_replace('/', '\\', trim($type, '\\')); + $qn = $sub . '\\' . $name . $suffix; + + if (class_exists('\Cake' . $qn)) { + $base = 'Cake'; + } + + return [ + 'fqn' => '\\' . $base . $qn, + 'namespace' => $base . $sub, + 'plugin' => $plugin, + 'class' => $name . $suffix, + 'name' => $name, + 'fullName' => $class + ]; + } + + /** + * Return list of fields to generate controls for. + * + * @param array $fields Fields list. + * @param \Cake\Datasource\SchemaInterface $schema Schema instance. + * @param \Cake\ORM\Table|null $modelObject Model object. + * @param array $takeFields Take fields. + * @param array $filterTypes Filter field types. + * @return \Cake\Collection\CollectionInterface + */ + public function filterFields($fields, $schema, $modelObject = null, $takeFields = [], $filterTypes = ['binary']) + { + $fields = collection($fields) + ->filter(function ($field) use ($schema, $filterTypes) { + return !in_array($schema->columnType($field), $filterTypes); + }); + + if (isset($modelObject) && $modelObject->hasBehavior('Tree')) { + $fields = $fields->reject(function ($field) { + return $field === 'lft' || $field === 'rght'; + }); + } + + if (!empty($takeFields)) { + $fields = $fields->take($takeFields); + } + + return $fields->toArray(); + } + + /** + * Get fields data for view template. + * + * @param array $fields Fields list. + * @param \Cake\Datasource\SchemaInterface $schema Schema instance. + * @param array $associations Associations data. + * @return array + */ + public function getViewFieldsData($fields, $schema, $associations) + { + $immediateAssociations = $associations['BelongsTo']; + $associationFields = collection($fields) + ->map(function ($field) use ($immediateAssociations) { + foreach ($immediateAssociations as $alias => $details) { + if ($field === $details['foreignKey']) { + return [$field => $details]; + } + } + }) + ->filter() + ->reduce(function ($fields, $value) { + return $fields + $value; + }, []); + + $groupedFields = collection($fields) + ->filter(function ($field) use ($schema) { + return $schema->columnType($field) !== 'binary'; + }) + ->groupBy(function ($field) use ($schema, $associationFields) { + $type = $schema->columnType($field); + if (isset($associationFields[$field])) { + return 'string'; + } + if (in_array($type, [ + 'decimal', + 'biginteger', + 'integer', + 'float', + 'smallinteger', + 'tinyinteger', + ])) { + return 'number'; + } + if (in_array($type, ['date', 'time', 'datetime', 'timestamp'])) { + return 'date'; + } + + return in_array($type, ['text', 'boolean']) ? $type : 'string'; + }) + ->toArray(); + + $groupedFields += [ + 'number' => [], + 'string' => [], + 'boolean' => [], + 'date' => [], + 'text' => [], + ]; + + return compact('associationFields', 'groupedFields'); + } + + /** + * Get column data from schema. + * + * @param string $field Field name. + * @param \Cake\Database\Schema\TableSchema $schema Schema. + * @return array + */ + public function columnData($field, $schema) + { + return $schema->column($field); + } + + /** + * Get alias of associated table. + * + * @param \Cake\ORM\Table $modelObj Model object. + * @param string $assoc Association name. + * @return string + */ + public function getAssociatedTableAlias($modelObj, $assoc) + { + $association = $modelObj->association($assoc); + + return $association->getTarget()->getAlias(); + } + + /** + * Get validation methods data. + * + * @param string $field Field name. + * @param array $rules Validation rules list. + * @return array + */ + public function getValidationMethods($field, $rules) + { + $validationMethods = []; + + foreach ($rules as $ruleName => $rule) { + if ($rule['rule'] && !isset($rule['provider']) && !isset($rule['args'])) { + $validationMethods[] = sprintf("->%s('%s')", $rule['rule'], $field); + } elseif ($rule['rule'] && !isset($rule['provider'])) { + $formatTemplate = "->%s('%s')"; + if (!empty($rule['args'])) { + $formatTemplate = "->%s('%s', %s)"; + } + $validationMethods[] = sprintf( + $formatTemplate, + $rule['rule'], + $field, + $this->stringifyList( + $rule['args'], + ['indent' => false, 'quotes' => false] + ) + ); + } elseif ($rule['rule'] && isset($rule['provider'])) { + $validationMethods[] = sprintf( + "->add('%s', '%s', ['rule' => '%s', 'provider' => '%s'])", + $field, + $ruleName, + $rule['rule'], + $rule['provider'] + ); + } + + if (isset($rule['allowEmpty'])) { + if (is_string($rule['allowEmpty'])) { + $validationMethods[] = sprintf( + "->allowEmpty('%s', '%s')", + $field, + $rule['allowEmpty'] + ); + } elseif ($rule['allowEmpty']) { + $validationMethods[] = sprintf( + "->allowEmpty('%s')", + $field + ); + } else { + $validationMethods[] = sprintf( + "->requirePresence('%s', 'create')", + $field + ); + $validationMethods[] = sprintf( + "->notEmpty('%s')", + $field + ); + } + } + } + + return $validationMethods; + } + + /** + * Get field accessibility data. + * + * @param mixed $fields Fields list. + * @param mixed $primaryKey Primary key. + * @return array + */ + public function getFieldAccessibility($fields = null, $primaryKey = null) + { + $accessible = []; + + if (!isset($fields) || $fields !== false) { + if (!empty($fields)) { + foreach ($fields as $field) { + $accessible[$field] = 'true'; + } + } elseif (!empty($primaryKey)) { + $accessible['*'] = 'true'; + foreach ($primaryKey as $field) { + $accessible[$field] = 'false'; + } + } + } + + return $accessible; + } + + /** + * Wrap string arguments with quotes + * + * @param array $args array of arguments + * @return array + */ + public function escapeArguments($args) + { + return array_map(function ($v) { + if (is_string($v)) { + $v = strtr($v, ["'" => "\'"]); + $v = "'$v'"; + } + + return $v; + }, $args); + } + + /** + * To be mocked elsewhere... + * + * @param \Cake\ORM\Table $table Table + * @param array $aliases array of aliases + * @return array + */ + protected function _filterHasManyAssociationsAliases($table, $aliases) + { + if (is_null($this->_associationFilter)) { + $this->_associationFilter = new AssociationFilter(); + } + + return $this->_associationFilter->filterHasManyAssociationsAliases($table, $aliases); + } +} diff --git a/app/vendor/cakephp/bake/src/View/Helper/DocBlockHelper.php b/app/vendor/cakephp/bake/src/View/Helper/DocBlockHelper.php new file mode 100644 index 000000000..064fcf5d4 --- /dev/null +++ b/app/vendor/cakephp/bake/src/View/Helper/DocBlockHelper.php @@ -0,0 +1,286 @@ + 1 && $ann[0] === '@' && strpos($ann, ' ') > 0) { + $type = substr($ann, 0, strpos($ann, ' ')); + if ($this->_annotationSpacing && + $previous !== false && + $previous !== $type + ) { + $lines[] = ''; + } + $previous = $type; + } + $lines[] = $ann; + } + + $lines = array_merge(["/**"], (new Collection($lines))->map(function ($line) { + return rtrim(" * {$line}"); + })->toArray(), [" */"]); + + return implode("\n", $lines); + } + + /** + * Converts an entity class type to its DocBlock hint type counterpart. + * + * @param string $type The entity class type (a fully qualified class name). + * @param \Cake\ORM\Association $association The association related to the entity class. + * @return string The DocBlock type + */ + public function associatedEntityTypeToHintType($type, Association $association) + { + if ($association->type() === Association::MANY_TO_MANY || + $association->type() === Association::ONE_TO_MANY + ) { + return $type . '[]'; + } + + return $type; + } + + /** + * Builds a map of entity columns as DocBlock types for use + * in generating `@property` hints. + * + * This method expects a property schema as generated by + * `\Bake\Shell\Task\ModelTask::getEntityPropertySchema()`. + * + * The generated map has the format of + * + * ``` + * [ + * 'property-name' => 'doc-block-type', + * ... + * ] + * ``` + * + * @see \Bake\Shell\Task\ModelTask::getEntityPropertySchema + * + * @param array $propertySchema The property schema to use for generating the type map. + * @return array The property DocType map. + */ + public function buildEntityPropertyHintTypeMap(array $propertySchema) + { + $properties = []; + foreach ($propertySchema as $property => $info) { + if ($info['kind'] === 'column') { + $properties[$property] = $this->columnTypeToHintType($info['type']); + } + } + + return $properties; + } + + /** + * Builds a map of entity associations as DocBlock types for use + * in generating `@property` hints. + * + * This method expects a property schema as generated by + * `\Bake\Shell\Task\ModelTask::getEntityPropertySchema()`. + * + * The generated map has the format of + * + * ``` + * [ + * 'property-name' => 'doc-block-type', + * ... + * ] + * ``` + * + * @see \Bake\Shell\Task\ModelTask::getEntityPropertySchema + * + * @param array $propertySchema The property schema to use for generating the type map. + * @return array The property DocType map. + */ + public function buildEntityAssociationHintTypeMap(array $propertySchema) + { + $properties = []; + foreach ($propertySchema as $property => $info) { + if ($info['kind'] === 'association') { + $type = $this->associatedEntityTypeToHintType($info['type'], $info['association']); + if ($info['association']->type() === Association::MANY_TO_ONE) { + $properties = $this->_insertAfter( + $properties, + $info['association']->foreignKey(), + [$property => $type] + ); + } else { + $properties[$property] = $type; + } + } + } + + return $properties; + } + + /** + * Converts a column type to its DocBlock type counterpart. + * + * This method only supports the default CakePHP column types, + * custom column/database types will be ignored. + * + * @see \Cake\Database\Type + * + * @param string $type The column type. + * @return null|string The DocBlock type, or `null` for unsupported column types. + */ + public function columnTypeToHintType($type) + { + switch ($type) { + case 'string': + case 'text': + case 'uuid': + return 'string'; + + case 'integer': + case 'biginteger': + case 'smallinteger': + case 'tinyinteger': + return 'int'; + + case 'float': + case 'decimal': + return 'float'; + + case 'boolean': + return 'bool'; + + case 'array': + case 'json': + return 'array'; + + case 'binary': + return 'string|resource'; + + case 'date': + case 'datetime': + case 'time': + case 'timestamp': + $dbType = Type::build($type); + if (method_exists($dbType, 'getDateTimeClassName')) { + return '\\' . Type::build($type)->getDateTimeClassName(); + } + + return '\Cake\I18n\Time'; + } + + return null; + } + + /** + * Renders a map of DocBlock property types as an array of + * `@property` hints. + * + * @param array $properties A key value pair where key is the name of a property and the value is the type. + * @return array + */ + public function propertyHints(array $properties) + { + $lines = []; + foreach ($properties as $property => $type) { + $type = $type ? $type . ' ' : ''; + $lines[] = "@property {$type}\${$property}"; + } + + return $lines; + } + + /** + * Build property, method, mixing annotations for table class. + * + * @param array $associations Associations list. + * @param array $associationInfo Association info. + * @param array $behaviors Behaviors list. + * @param string $entity Entity name. + * @param string $namespace Namespace. + * @return array + */ + public function buildTableAnnotations($associations, $associationInfo, $behaviors, $entity, $namespace) + { + $annotations = []; + foreach ($associations as $type => $assocs) { + foreach ($assocs as $assoc) { + $typeStr = Inflector::camelize($type); + $tableFqn = $associationInfo[$assoc['alias']]['targetFqn']; + $annotations[] = "@property {$tableFqn}|\Cake\ORM\Association\\{$typeStr} \${$assoc['alias']}"; + } + } + $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} get(\$primaryKey, \$options = [])"; + $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} newEntity(\$data = null, array \$options = [])"; + $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[] newEntities(array \$data, array \$options = [])"; + $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}|bool save(\\Cake\\Datasource\\EntityInterface \$entity, \$options = [])"; + $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} patchEntity(\\Cake\\Datasource\\EntityInterface \$entity, array \$data, array \$options = [])"; + $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[] patchEntities(\$entities, array \$data, array \$options = [])"; + $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} findOrCreate(\$search, callable \$callback = null, \$options = [])"; + foreach ($behaviors as $behavior => $behaviorData) { + $className = App::className($behavior, 'Model/Behavior', 'Behavior'); + if ($className === false) { + $className = "Cake\ORM\Behavior\\{$behavior}Behavior"; + } + + $annotations[] = '@mixin \\' . $className; + } + + return $annotations; + } + + /** + * Inserts a value after a specific key in an associative array. + * + * In case the given key cannot be found, the value will be appended. + * + * @param array $target The array in which to insert the new value. + * @param string $key The array key after which to insert the new value. + * @param mixed $value The entry to insert. + * @return array The array with the new value inserted. + */ + protected function _insertAfter(array $target, $key, $value) + { + $index = array_search($key, array_keys($target)); + if ($index !== false) { + $target = array_merge( + array_slice($target, 0, $index + 1), + $value, + array_slice($target, $index + 1, null) + ); + } else { + $target += (array)$value; + } + + return $target; + } +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/BakeArticlesBakeTagsFixture.php b/app/vendor/cakephp/bake/tests/Fixture/BakeArticlesBakeTagsFixture.php new file mode 100644 index 000000000..00ad9210f --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/BakeArticlesBakeTagsFixture.php @@ -0,0 +1,42 @@ + ['type' => 'integer', 'null' => false], + 'bake_tag_id' => ['type' => 'integer', 'null' => false], + '_constraints' => ['UNIQUE_TAG' => ['type' => 'unique', 'columns' => ['bake_article_id', 'bake_tag_id']]] + ]; + + /** + * records property + * + * @var array + */ + public $records = []; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/BakeArticlesFixture.php b/app/vendor/cakephp/bake/tests/Fixture/BakeArticlesFixture.php new file mode 100644 index 000000000..07a5ebc4c --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/BakeArticlesFixture.php @@ -0,0 +1,47 @@ + ['type' => 'integer'], + 'bake_user_id' => ['type' => 'integer', 'null' => false], + 'title' => ['type' => 'string', 'length' => 50, 'null' => false], + 'body' => 'text', + 'published' => ['type' => 'boolean', 'length' => 1, 'default' => false], + 'created' => 'datetime', + 'updated' => 'datetime', + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]] + ]; + + /** + * records property + * + * @var array + */ + public $records = []; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/BakeCarFixture.php b/app/vendor/cakephp/bake/tests/Fixture/BakeCarFixture.php new file mode 100644 index 000000000..730b2f41a --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/BakeCarFixture.php @@ -0,0 +1,53 @@ + ['type' => 'integer'], + 'bake_user_id' => ['type' => 'integer', 'null' => false], + 'title' => ['type' => 'string', 'null' => false], + 'body' => 'text', + 'published' => ['type' => 'boolean', 'length' => 1, 'default' => false], + 'created' => 'datetime', + 'updated' => 'datetime', + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]] + ]; + + /** + * records property + * + * @var array + */ + public $records = []; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/BakeCommentsFixture.php b/app/vendor/cakephp/bake/tests/Fixture/BakeCommentsFixture.php new file mode 100644 index 000000000..d5d7119af --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/BakeCommentsFixture.php @@ -0,0 +1,47 @@ + ['type' => 'integer'], + 'bake_article_id' => ['type' => 'integer', 'null' => false], + 'bake_user_id' => ['type' => 'integer', 'null' => false], + 'comment' => 'text', + 'published' => ['type' => 'string', 'length' => 1, 'default' => 'N'], + 'created' => 'datetime', + 'updated' => 'datetime', + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['otherid']]] + ]; + + /** + * records property + * + * @var array + */ + public $records = []; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/BakeTagsFixture.php b/app/vendor/cakephp/bake/tests/Fixture/BakeTagsFixture.php new file mode 100644 index 000000000..6dd33d6ee --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/BakeTagsFixture.php @@ -0,0 +1,44 @@ + ['type' => 'integer'], + 'tag' => ['type' => 'string', 'null' => false], + 'created' => 'datetime', + 'updated' => 'datetime', + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]] + ]; + + /** + * records property + * + * @var array + */ + public $records = []; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/BakeTemplateAuthorsFixture.php b/app/vendor/cakephp/bake/tests/Fixture/BakeTemplateAuthorsFixture.php new file mode 100644 index 000000000..ea68492e7 --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/BakeTemplateAuthorsFixture.php @@ -0,0 +1,61 @@ + ['type' => 'integer'], + 'role_id' => ['type' => 'integer', 'null' => false], + 'name' => ['type' => 'string', 'default' => null], + 'description' => ['type' => 'text', 'default' => null], + 'member' => ['type' => 'boolean'], + 'member_number' => ['type' => 'integer', 'null' => true], + 'account_balance' => ['type' => 'decimal', 'null' => true, 'precision' => 2, 'length' => 12], + 'created' => 'datetime', + 'modified' => 'datetime', + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]] + ]; + + /** + * records property + * + * @var array + */ + public $records = [ + ['name' => 'mariano', 'role_id' => 1], + ['name' => 'nate', 'role_id' => 2], + ['name' => 'larry', 'role_id' => 2], + ['name' => 'garrett', 'role_id' => 1], + ]; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/BakeTemplateProfilesFixture.php b/app/vendor/cakephp/bake/tests/Fixture/BakeTemplateProfilesFixture.php new file mode 100644 index 000000000..0633e9b40 --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/BakeTemplateProfilesFixture.php @@ -0,0 +1,57 @@ + ['type' => 'integer'], + 'author_id' => ['type' => 'integer', 'null' => false], + 'nick' => ['type' => 'string', 'null' => false], + 'avatar' => ['type' => 'string', 'default' => null], + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]] + ]; + + /** + * records property + * + * @var array + */ + public $records = [ + ['author_id' => 1, 'nick' => 'The Comedian', 'avatar' => 'smiley.png'], + ['author_id' => 2, 'nick' => 'Rorschach', 'avatar' => 'stains.png'], + ['author_id' => 3, 'nick' => 'Ozymandias', 'avatar' => null], + ['author_id' => 4, 'nick' => 'Dr. Manhattan', 'avatar' => 'blue_lightning.png'], + ]; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/BakeTemplateRolesFixture.php b/app/vendor/cakephp/bake/tests/Fixture/BakeTemplateRolesFixture.php new file mode 100644 index 000000000..8b486b94e --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/BakeTemplateRolesFixture.php @@ -0,0 +1,49 @@ + ['type' => 'integer'], + 'name' => ['type' => 'string', 'null' => false], + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]] + ]; + + /** + * records property + * + * @var array + */ + public $records = [ + ['name' => 'admin'], + ['name' => 'user'], + ]; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/BinaryTestsFixture.php b/app/vendor/cakephp/bake/tests/Fixture/BinaryTestsFixture.php new file mode 100644 index 000000000..f0ead760b --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/BinaryTestsFixture.php @@ -0,0 +1,42 @@ + ['type' => 'integer'], + 'data' => ['type' => 'binary', 'length' => 300], + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]] + ]; + + /** + * records property + * + * @var array + */ + public $records = []; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/CategoriesFixture.php b/app/vendor/cakephp/bake/tests/Fixture/CategoriesFixture.php new file mode 100644 index 000000000..ff56bafea --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/CategoriesFixture.php @@ -0,0 +1,59 @@ + ['type' => 'integer', 'length' => 11, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], + 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'name' => ['type' => 'string', 'length' => 100, 'null' => false, 'default' => '', 'comment' => '', 'precision' => null, 'fixed' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + '_options' => [ + 'engine' => 'InnoDB', + 'collation' => 'utf8_general_ci' + ], + ]; + // @codingStandardsIgnoreEnd + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'created' => '2015-12-30 18:11:36', + 'modified' => '2015-12-30 18:11:36', + 'name' => 'Lorem ipsum dolor sit amet' + ], + ]; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/CategoriesProductsFixture.php b/app/vendor/cakephp/bake/tests/Fixture/CategoriesProductsFixture.php new file mode 100644 index 000000000..ce781ec65 --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/CategoriesProductsFixture.php @@ -0,0 +1,56 @@ + ['type' => 'integer', 'length' => 11, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'autoIncrement' => null], + 'product_id' => ['type' => 'integer', 'length' => 11, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'autoIncrement' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['category_id', 'product_id'], 'length' => []], + ], + '_options' => [ + 'engine' => 'InnoDB', + 'collation' => 'utf8_general_ci' + ], + ]; + // @codingStandardsIgnoreEnd + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'category_id' => 1, + 'product_id' => 1 + ], + ]; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/CategoryThreadsFixture.php b/app/vendor/cakephp/bake/tests/Fixture/CategoryThreadsFixture.php new file mode 100644 index 000000000..9881936a5 --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/CategoryThreadsFixture.php @@ -0,0 +1,55 @@ + ['type' => 'integer'], + 'parent_id' => ['type' => 'integer'], + 'name' => ['type' => 'string', 'null' => false], + 'lft' => ['type' => 'integer'], + 'rght' => ['type' => 'integer'], + 'created' => 'datetime', + 'updated' => 'datetime', + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]] + ]; + + /** + * records property + * + * @var array + */ + public $records = [ + ['parent_id' => 0, 'name' => 'Category 1', 'lft' => 1, 'rght' => 14, 'created' => '2007-03-18 15:30:23', 'updated' => '2007-03-18 15:32:31'], + ['parent_id' => 1, 'name' => 'Category 1.1', 'lft' => 2, 'rght' => 9, 'created' => '2007-03-18 15:30:23', 'updated' => '2007-03-18 15:32:31'], + ['parent_id' => 2, 'name' => 'Category 1.1.1', 'lft' => 3, 'rght' => 8, 'created' => '2007-03-18 15:30:23', 'updated' => '2007-03-18 15:32:31'], + ['parent_id' => 3, 'name' => 'Category 1.1.2', 'lft' => 4, 'rght' => 7, 'created' => '2007-03-18 15:30:23', 'updated' => '2007-03-18 15:32:31'], + ['parent_id' => 4, 'name' => 'Category 1.1.1.1', 'lft' => 5, 'rght' => 6, 'created' => '2007-03-18 15:30:23', 'updated' => '2007-03-18 15:32:31'], + ['parent_id' => 5, 'name' => 'Category 2', 'lft' => 10, 'rght' => 13, 'created' => '2007-03-18 15:30:23', 'updated' => '2007-03-18 15:32:31'], + ['parent_id' => 6, 'name' => 'Category 2.1', 'lft' => 11, 'rght' => 12, 'created' => '2007-03-18 15:30:23', 'updated' => '2007-03-18 15:32:31'] + ]; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/DatatypesFixture.php b/app/vendor/cakephp/bake/tests/Fixture/DatatypesFixture.php new file mode 100644 index 000000000..62883c221 --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/DatatypesFixture.php @@ -0,0 +1,50 @@ + ['type' => 'integer', 'null' => false], + 'decimal_field' => ['type' => 'decimal', 'length' => '6', 'precision' => 3, 'default' => '0.000'], + 'float_field' => ['type' => 'float', 'length' => '5,2', 'null' => false, 'default' => null], + 'huge_int' => ['type' => 'biginteger'], + 'small_int' => ['type' => 'smallinteger'], + 'tiny_int' => ['type' => 'tinyinteger'], + 'bool' => ['type' => 'boolean', 'null' => false, 'default' => false], + 'uuid' => ['type' => 'uuid'], + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]] + ]; + + /** + * Records property + * + * @var array + */ + public $records = [ + ['float_field' => 42.23, 'huge_int' => '1234567891234567891', 'small_int' => '1234', 'tiny_int' => '12', 'bool' => 0], + ]; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/InvitationsFixture.php b/app/vendor/cakephp/bake/tests/Fixture/InvitationsFixture.php new file mode 100644 index 000000000..6d0b08841 --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/InvitationsFixture.php @@ -0,0 +1,68 @@ + ['type' => 'integer'], + 'sender_id' => ['type' => 'integer', 'null' => false], + 'receiver_id' => ['type' => 'integer', 'null' => false], + 'body' => 'text', + 'created' => 'datetime', + 'updated' => 'datetime', + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + 'sender_idx' => [ + 'type' => 'foreign', + 'columns' => ['sender_id'], + 'references' => ['users', 'id'], + 'update' => 'noAction', + 'delete' => 'noAction', + ], + 'receiver_idx' => [ + 'type' => 'foreign', + 'columns' => ['receiver_id'], + 'references' => ['users', 'id'], + 'update' => 'noAction', + 'delete' => 'noAction', + ], + ] + ]; + + /** + * records property + * + * @var array + */ + public $records = [ + [ + 'sender_id' => 1, + 'receiver_id' => 1, + 'body' => 'Try it out!', + ] + ]; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/NumberTreesFixture.php b/app/vendor/cakephp/bake/tests/Fixture/NumberTreesFixture.php new file mode 100644 index 000000000..d06fff4c3 --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/NumberTreesFixture.php @@ -0,0 +1,48 @@ + ['type' => 'integer'], + 'name' => ['type' => 'string', 'length' => 50, 'null' => false], + 'parent_id' => 'integer', + 'lft' => ['type' => 'integer'], + 'rght' => ['type' => 'integer'], + 'depth' => ['type' => 'integer'], + '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]] + ]; + + /** + * Records + * + * @var array + */ + public $records = []; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/OldProductsFixture.php b/app/vendor/cakephp/bake/tests/Fixture/OldProductsFixture.php new file mode 100644 index 000000000..068c9ad5f --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/OldProductsFixture.php @@ -0,0 +1,59 @@ + ['type' => 'integer', 'length' => 11, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], + 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'modified' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'name' => ['type' => 'string', 'length' => 100, 'null' => false, 'default' => '', 'comment' => '', 'precision' => null, 'fixed' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + '_options' => [ + 'engine' => 'InnoDB', + 'collation' => 'utf8_general_ci' + ], + ]; + // @codingStandardsIgnoreEnd + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'created' => '2015-12-30 18:11:36', + 'modified' => '2015-12-30 18:11:36', + 'name' => 'Lorem ipsum dolor sit amet' + ], + ]; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/ProductVersionsFixture.php b/app/vendor/cakephp/bake/tests/Fixture/ProductVersionsFixture.php new file mode 100644 index 000000000..6f480dc26 --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/ProductVersionsFixture.php @@ -0,0 +1,57 @@ + ['type' => 'integer', 'length' => 11, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], + 'product_id' => ['type' => 'integer', 'length' => 11, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'autoIncrement' => null], + 'version' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + '_options' => [ + 'engine' => 'InnoDB', + 'collation' => 'utf8_general_ci' + ], + ]; + // @codingStandardsIgnoreEnd + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'product_id' => 1, + 'version' => '2015-12-30 18:11:37' + ], + ]; +} diff --git a/app/vendor/cakephp/bake/tests/Fixture/ProductsFixture.php b/app/vendor/cakephp/bake/tests/Fixture/ProductsFixture.php new file mode 100644 index 000000000..2076f2119 --- /dev/null +++ b/app/vendor/cakephp/bake/tests/Fixture/ProductsFixture.php @@ -0,0 +1,59 @@ + ['type' => 'integer', 'length' => 11, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], + 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], + 'name' => ['type' => 'string', 'length' => 100, 'null' => false, 'default' => '', 'comment' => '', 'precision' => null, 'fixed' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], + ], + '_options' => [ + 'engine' => 'InnoDB', + 'collation' => 'utf8_general_ci' + ], + ]; + // @codingStandardsIgnoreEnd + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'created' => '2015-12-30 18:11:37', + 'modified' => '2015-12-30 18:11:37', + 'name' => 'Lorem ipsum dolor sit amet' + ], + ]; +} diff --git a/app/vendor/cakephp/bake/tests/bootstrap.php b/app/vendor/cakephp/bake/tests/bootstrap.php new file mode 100644 index 000000000..6892be7d8 --- /dev/null +++ b/app/vendor/cakephp/bake/tests/bootstrap.php @@ -0,0 +1,63 @@ + 'App', + 'paths' => [ + 'plugins' => [ROOT . 'Plugin' . DS], + 'templates' => [ROOT . 'App' . DS . 'Template' . DS] + ], + 'encoding' => 'UTF-8' +]); + +if (!getenv('db_dsn')) { + putenv('db_dsn=sqlite:///:memory:'); +} +ConnectionManager::config('test', ['url' => getenv('db_dsn')]); + +Plugin::load('Bake', [ + 'path' => dirname(dirname(__FILE__)) . DS, +]); + +class_alias('PHPUnit\Framework\TestCase', 'PHPUnit_Framework_TestCase'); diff --git a/app/vendor/cakephp/cakephp-codesniffer/.editorconfig b/app/vendor/cakephp/cakephp-codesniffer/.editorconfig new file mode 100644 index 000000000..706190175 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/.editorconfig @@ -0,0 +1,18 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.bat] +end_of_line = crlf + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/app/vendor/cakephp/cakephp-codesniffer/.gitattributes b/app/vendor/cakephp/cakephp-codesniffer/.gitattributes new file mode 100644 index 000000000..af6a86494 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/.gitattributes @@ -0,0 +1,10 @@ +# Define the line ending behavior of the different file extensions +# Set default behaviour, in case users don't have core.autocrlf set. +* text=lf + +# Explicitly declare text files we want to always be normalized and converted +# to native line endings on checkout. +*.php eol=lf + +# Declare files that will always have CRLF line endings on checkout. +*.bat eol=crlf diff --git a/app/vendor/cakephp/cakephp-codesniffer/.gitignore b/app/vendor/cakephp/cakephp-codesniffer/.gitignore new file mode 100644 index 000000000..a14dfb32d --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/.gitignore @@ -0,0 +1,5 @@ +/build/ +/dist/ +tags +composer.lock +vendor/ diff --git a/app/vendor/cakephp/cakephp-codesniffer/.travis.yml b/app/vendor/cakephp/cakephp-codesniffer/.travis.yml new file mode 100644 index 000000000..c591d1461 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/.travis.yml @@ -0,0 +1,38 @@ +language: php + +php: + - 7.1 + - 7.0 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + +sudo: false + +matrix: + fast_finish: true + + include: + - php: 7.1 + env: PHPCS=1 + + allow_failures: + - php: hhvm + +before_script: + - composer self-update + - composer install --prefer-source + +script: + - if [[ $PHPCS != 1 && $TRAVIS_PHP_VERSION = 7.1 ]]; then composer run-script test-coverage --timeout=0; fi + - if [[ $PHPCS != 1 && $TRAVIS_PHP_VERSION != 7.1 ]]; then composer test; fi + - if [[ $PHPCS = 1 ]]; then composer cs-check; fi + +after_success: + - if [[ $PHPCS != 1 && $TRAVIS_PHP_VERSION = 7.1 ]]; then bash <(curl -s https://codecov.io/bash); fi + +notifications: + email: false diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php new file mode 100644 index 000000000..528019c18 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Commenting/DocBlockAlignmentSniff.php @@ -0,0 +1,88 @@ +getTokens(); + $commentClose = $phpcsFile->findNext(T_DOC_COMMENT_CLOSE_TAG, $stackPtr); + $afterComment = $phpcsFile->findNext(T_WHITESPACE, $commentClose + 1, null, true); + $commentIndentation = $tokens[$stackPtr]['column'] - 1; + $codeIndentation = $tokens[$afterComment]['column'] - 1; + + // Check for doc block opening being misaligned + if ($commentIndentation != $codeIndentation) { + $msg = 'Doc block not aligned with code; expected indentation of %s but found %s'; + $data = [$codeIndentation, $commentIndentation]; + $fix = $phpcsFile->addFixableError($msg, $stackPtr, 'DocBlockMisaligned', $data); + if ($fix === true) { + // Collect tokens to change indentation of + $tokensToIndent = [ + $stackPtr => $codeIndentation + ]; + $commentOpenLine = $tokens[$stackPtr]['line']; + $commentCloseLine = $tokens[$commentClose]['line']; + $lineBreaksInComment = $commentCloseLine - $commentOpenLine; + if ($lineBreaksInComment !== 0) { + $searchToken = $stackPtr + 1; + do { + $commentBorder = $phpcsFile->findNext( + [T_DOC_COMMENT_STAR, T_DOC_COMMENT_CLOSE_TAG], + $searchToken, + $commentClose + 1 + ); + if ($commentBorder !== false) { + $tokensToIndent[$commentBorder] = $codeIndentation + 1; + $searchToken = $commentBorder + 1; + } + } while ($commentBorder !== false); + } + + // Update indentation + $phpcsFile->fixer->beginChangeset(); + foreach ($tokensToIndent as $searchToken => $indent) { + $indentString = str_repeat(' ', $indent); + $isOpenTag = $tokens[$searchToken]['type'] === 'T_DOC_COMMENT_OPEN_TAG'; + if ($isOpenTag && $commentIndentation === 0) { + $phpcsFile->fixer->addContentBefore($searchToken, $indentString); + } else { + $phpcsFile->fixer->replaceToken($searchToken - 1, $indentString); + } + } + $phpcsFile->fixer->endChangeset(); + } + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Commenting/FunctionCommentSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Commenting/FunctionCommentSniff.php new file mode 100644 index 000000000..c1749faf9 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Commenting/FunctionCommentSniff.php @@ -0,0 +1,477 @@ + + * @author Marc McIntyre + * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @link http://pear.php.net/package/PHP_CodeSniffer + */ +namespace CakePHP\Sniffs\Commenting; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FunctionCommentSniff as PearFunctionCommentSniff; +use PHP_CodeSniffer\Util\Common; + +/** + * Parses and verifies the doc comments for functions. + * + * Verifies that : + *
    + *
  • A comment exists
  • + *
  • There is a blank newline after the short description
  • + *
  • There is a blank newline between the long and short description
  • + *
  • There is a blank newline between the long description and tags
  • + *
  • Parameter names represent those in the method
  • + *
  • Parameter comments are in the correct order
  • + *
  • Parameter comments are complete
  • + *
  • A type hint is provided for array and custom class
  • + *
  • Type hint matches the actual variable/class type
  • + *
  • A blank line is present before the first and after the last parameter
  • + *
  • A return type exists
  • + *
  • Any throw tag must have a comment
  • + *
  • The tag order and indentation are correct
  • + *
+ * + * @category PHP + * @package PHP_CodeSniffer + * @author Greg Sherwood + * @author Marc McIntyre + * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @version Release: @package_version@ + * @link http://pear.php.net/package/PHP_CodeSniffer + */ +class FunctionCommentSniff extends PearFunctionCommentSniff +{ + /** + * Is the comment an inheritdoc? + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @return bool True if the comment is an inheritdoc + */ + protected function isInheritDoc(File $phpcsFile, $stackPtr) + { + $start = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, $stackPtr - 1); + $end = $phpcsFile->findNext(T_DOC_COMMENT_CLOSE_TAG, $start); + $content = $phpcsFile->getTokensAsString($start, ($end - $start)); + + return preg_match('/{@inheritDoc}/i', $content) === 1; + } // end isInheritDoc() + + /** + * Process the return comment of this function comment. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @param int $commentStart The position in the stack where the comment started. + * @return void + */ + protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) + { + if ($this->isInheritDoc($phpcsFile, $stackPtr)) { + return; + } + + $tokens = $phpcsFile->getTokens(); + + // Skip constructor and destructor. + $className = ''; + foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) { + if ($condition === T_CLASS || $condition === T_INTERFACE) { + $className = $phpcsFile->getDeclarationName($condPtr); + $className = strtolower(ltrim($className, '_')); + } + } + + $methodName = $phpcsFile->getDeclarationName($stackPtr); + $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct'); + if ($methodName !== '_') { + $methodName = strtolower(ltrim($methodName, '_')); + } + + $return = null; + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + if ($tokens[$tag]['content'] === '@return') { + if ($return !== null) { + $error = 'Only 1 @return tag is allowed in a function comment'; + $phpcsFile->addError($error, $tag, 'DuplicateReturn'); + + return; + } + + $return = $tag; + } + } + + if ($isSpecialMethod === true) { + return; + } + + if ($return === null) { + $error = 'Missing @return tag in function comment'; + $phpcsFile->addWarning($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn'); + + return; + }//end if + + $content = $tokens[($return + 2)]['content']; + if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) { + $error = 'Return type missing for @return tag in function comment'; + $phpcsFile->addError($error, $return, 'MissingReturnType'); + + return; + } + + // Check return type (can be multiple, separated by '|'). + list($types, ) = explode(' ', $content); + $typeNames = explode('|', $types); + $suggestedNames = []; + foreach ($typeNames as $i => $typeName) { + if ($typeName === 'integer') { + $suggestedName = 'int'; + } elseif ($typeName === 'boolean') { + $suggestedName = 'bool'; + } elseif (in_array($typeName, ['int', 'bool'])) { + $suggestedName = $typeName; + } else { + $suggestedName = Common::suggestType($typeName); + } + if (in_array($suggestedName, $suggestedNames) === false) { + $suggestedNames[] = $suggestedName; + } + } + + $suggestedType = implode('|', $suggestedNames); + if ($types !== $suggestedType) { + $error = 'Expected "%s" but found "%s" for function return type'; + $data = [ + $suggestedType, + $types, + ]; + $phpcsFile->addError($error, $return, 'InvalidReturn', $data); + } + + $endToken = isset($tokens[$stackPtr]['scope_closer']) ? $tokens[$stackPtr]['scope_closer'] : false; + if (!$endToken) { + return; + } + + // If the return type is void, make sure there is + // no non-void return statements in the function. + if ($typeNames === ['void']) { + for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) { + if ($tokens[$returnToken]['code'] === T_CLOSURE) { + $returnToken = $tokens[$returnToken]['scope_closer']; + continue; + } + + if ($tokens[$returnToken]['code'] === T_RETURN + || $tokens[$returnToken]['code'] === T_YIELD + ) { + break; + } + } + + if ($returnToken !== $endToken) { + // If the function is not returning anything, just + // exiting, then there is no problem. + $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); + if ($tokens[$semicolon]['code'] !== T_SEMICOLON) { + $error = 'Function return type is void, but function contains return statement'; + $phpcsFile->addWarning($error, $return, 'InvalidReturnVoid'); + } + } + + return; + } + + // If return type is not void, there needs to be a return statement + // somewhere in the function that returns something. + if (!in_array('mixed', $typeNames, true) && !in_array('void', $typeNames, true)) { + $returnToken = $phpcsFile->findNext([T_RETURN, T_YIELD], $stackPtr, $endToken); + if ($returnToken === false) { + $error = 'Function return type is not void, but function has no return statement'; + $phpcsFile->addWarning($error, $return, 'InvalidNoReturn'); + } else { + $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); + if ($tokens[$semicolon]['code'] === T_SEMICOLON) { + $error = 'Function return type is not void, but function is returning void here'; + $phpcsFile->addWarning($error, $returnToken, 'InvalidReturnNotVoid'); + } + } + }//end if + }//end processReturn() + + + /** + * Process any throw tags that this function comment has. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @param int $commentStart The position in the stack where the comment started. + * @return void + */ + protected function processThrows(File $phpcsFile, $stackPtr, $commentStart) + { + $tokens = $phpcsFile->getTokens(); + + foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { + if ($tokens[$tag]['content'] !== '@throws') { + continue; + } + + $exception = $comment = null; + if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { + $matches = []; + preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[($tag + 2)]['content'], $matches); + $exception = $matches[1]; + if (isset($matches[2]) === true) { + $comment = $matches[2]; + } + } + + if ($exception === null) { + $error = 'Exception type and comment missing for @throws tag in function comment'; + $phpcsFile->addWarning($error, $tag, 'InvalidThrows'); + } elseif ($comment === null) { + $error = 'Comment missing for @throws tag in function comment'; + $phpcsFile->addWarning($error, $tag, 'EmptyThrows'); + } else { + // Any strings until the next tag belong to this comment. + if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { + $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; + } else { + $end = $tokens[$commentStart]['comment_closer']; + } + + for ($i = ($tag + 3); $i < $end; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + $comment .= ' ' . $tokens[$i]['content']; + } + } + + // Starts with a capital letter and ends with a fullstop. + $firstChar = $comment{0}; + if (strtoupper($firstChar) !== $firstChar) { + $error = '@throws tag comment must start with a capital letter'; + $phpcsFile->addWarning($error, ($tag + 2), 'ThrowsNotCapital'); + } + + $lastChar = substr($comment, -1); + if ($lastChar !== '.') { + $error = '@throws tag comment must end with a full stop'; + $phpcsFile->addWarning($error, ($tag + 2), 'ThrowsNoFullStop'); + } + }//end if + }//end foreach + }//end processThrows() + + + /** + * Process the function parameter comments. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @param int $commentStart The position in the stack where the comment started. + * @return void + */ + protected function processParams(File $phpcsFile, $stackPtr, $commentStart) + { + if ($this->isInheritDoc($phpcsFile, $stackPtr)) { + return; + } + + $tokens = $phpcsFile->getTokens(); + + $params = []; + $maxType = $maxVar = 0; + foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { + if ($tokens[$tag]['content'] !== '@param') { + continue; + } + + $type = $var = $comment = ''; + $typeSpace = $varSpace = 0; + $commentLines = []; + if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { + $matches = []; + preg_match('/([^$&]+)(?:((?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches); + + $typeLen = strlen($matches[1]); + $type = trim($matches[1]); + $typeSpace = ($typeLen - strlen($type)); + $typeLen = strlen($type); + if ($typeLen > $maxType) { + $maxType = $typeLen; + } + + if (isset($matches[2]) === true) { + $var = $matches[2]; + $varLen = strlen($var); + if ($varLen > $maxVar) { + $maxVar = $varLen; + } + + if (isset($matches[4]) === true) { + $varSpace = strlen($matches[3]); + $comment = $matches[4]; + $commentLines[] = [ + 'comment' => $comment, + 'token' => ($tag + 2), + 'indent' => $varSpace, + ]; + + // Any strings until the next tag belong to this comment. + if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { + $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; + } else { + $end = $tokens[$commentStart]['comment_closer']; + } + + for ($i = ($tag + 3); $i < $end; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + $indent = 0; + if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) { + $indent = strlen($tokens[($i - 1)]['content']); + } + + $comment .= ' ' . $tokens[$i]['content']; + $commentLines[] = [ + 'comment' => $tokens[$i]['content'], + 'token' => $i, + 'indent' => $indent, + ]; + } + } + } else { + $error = 'Missing parameter comment'; + $phpcsFile->addError($error, $tag, 'MissingParamComment'); + $commentLines[] = ['comment' => '']; + }//end if + } else { + $error = 'Missing parameter name'; + $phpcsFile->addError($error, $tag, 'MissingParamName'); + }//end if + } else { + $error = 'Missing parameter type'; + $phpcsFile->addError($error, $tag, 'MissingParamType'); + }//end if + + $params[] = compact('tag', 'type', 'var', 'comment', 'commentLines', 'typeSpace', 'varSpace'); + }//end foreach + + $realParams = $phpcsFile->getMethodParameters($stackPtr); + $foundParams = []; + + foreach ($params as $pos => $param) { + // If the type is empty, the whole line is empty. + if ($param['type'] === '') { + continue; + } + + // Check the param type value. + $typeNames = explode('|', $param['type']); + foreach ($typeNames as $typeName) { + if ($typeName === 'integer') { + $suggestedName = 'int'; + } elseif ($typeName === 'boolean') { + $suggestedName = 'bool'; + } elseif (in_array($typeName, ['int', 'bool'])) { + $suggestedName = $typeName; + } else { + $suggestedName = Common::suggestType($typeName); + } + + if ($typeName !== $suggestedName) { + $error = 'Expected "%s" but found "%s" for parameter type'; + $data = [$suggestedName, $typeName]; + + $fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data); + if ($fix === true) { + $content = $suggestedName; + $content .= str_repeat(' ', $param['typeSpace']); + $content .= $param['var']; + $content .= str_repeat(' ', $param['varSpace']); + $content .= $param['commentLines'][0]['comment']; + $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); + } + } + }//end foreach + + if ($param['var'] === '') { + continue; + } + + $foundParams[] = $param['var']; + + // Make sure the param name is correct. + if (isset($realParams[$pos]) === true) { + $realName = $realParams[$pos]['name']; + if ($realName !== $param['var']) { + $code = 'ParamNameNoMatch'; + $data = [$param['var'], $realName]; + + $error = 'Doc comment for parameter %s does not match '; + if (strtolower($param['var']) === strtolower($realName)) { + $error .= 'case of '; + $code = 'ParamNameNoCaseMatch'; + } + + $error .= 'actual variable name %s'; + + $fix = $phpcsFile->addFixableWarning($error, $param['tag'], $code, $data); + + if ($fix === true) { + $content = $suggestedName; + $content .= str_repeat(' ', $param['typeSpace']); + $content .= $realName; + $content .= str_repeat(' ', $param['varSpace']); + $content .= $param['commentLines'][0]['comment']; + $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); + } + } + } elseif (substr($param['var'], -4) !== ',...') { + // We must have an extra parameter comment. + $error = 'Superfluous parameter comment'; + $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment'); + }//end if + + if ($param['comment'] === '') { + continue; + } + + // Param comments must start with a capital letter and end with the full stop. + $firstChar = $param['comment']{0}; + if (preg_match('|\p{Lu}|u', $firstChar) === 0) { + $error = 'Parameter comment must start with a capital letter'; + $phpcsFile->addWarning($error, $param['tag'], 'ParamCommentNotCapital'); + } + + $lastChar = substr($param['comment'], -1); + if ($lastChar !== '.') { + $error = 'Parameter comment must end with a full stop'; + $phpcsFile->addWarning($error, $param['tag'], 'ParamCommentFullStop'); + } + }//end foreach + + $realNames = []; + foreach ($realParams as $realParam) { + $realNames[] = $realParam['name']; + } + + // Report missing comments. + $diff = array_diff($realNames, $foundParams); + foreach ($diff as $neededParam) { + $error = 'Doc comment for parameter "%s" missing'; + $data = [$neededParam]; + $phpcsFile->addWarning($error, $commentStart, 'MissingParamTag', $data); + } + }//end processParams() +}//end class diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/ControlStructures/ControlStructuresSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/ControlStructures/ControlStructuresSniff.php new file mode 100644 index 000000000..0727230c9 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/ControlStructures/ControlStructuresSniff.php @@ -0,0 +1,62 @@ +getTokens(); + + $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if ($tokens[$nextToken]['code'] === T_OPEN_PARENTHESIS) { + $closer = $tokens[$nextToken]['parenthesis_closer']; + $diff = $closer - $stackPtr; + $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + $diff + 1), null, true); + } + if ($tokens[$nextToken]['code'] === T_IF) { + // "else if" is not checked by this sniff, another sniff takes care of that. + return; + } + if ($tokens[$nextToken]['code'] !== T_OPEN_CURLY_BRACKET && $tokens[$nextToken]['code'] !== T_COLON) { + $error = 'Curly brackets required for if/elseif/else.'; + $phpcsFile->addError($error, $stackPtr, 'NotAllowed'); + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/ControlStructures/ElseIfDeclarationSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/ControlStructures/ElseIfDeclarationSniff.php new file mode 100644 index 000000000..fa1816e11 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/ControlStructures/ElseIfDeclarationSniff.php @@ -0,0 +1,53 @@ +getTokens(); + + $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if ($tokens[$nextToken]['code'] !== T_IF) { + return; + } + + $error = 'Usage of ELSE IF not allowed; use ELSEIF instead'; + $phpcsFile->addError($error, $stackPtr, 'NotAllowed'); + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/ControlStructures/WhileStructuresSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/ControlStructures/WhileStructuresSniff.php new file mode 100644 index 000000000..42045f8a7 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/ControlStructures/WhileStructuresSniff.php @@ -0,0 +1,61 @@ +getTokens(); + + $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if ($tokens[$nextToken]['code'] === T_OPEN_PARENTHESIS) { + $closer = $tokens[$nextToken]['parenthesis_closer']; + $diff = $closer - $stackPtr; + $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + $diff + 1), null, true); + } + + if ($tokens[$stackPtr]['code'] === T_WHILE && $tokens[$nextToken]['code'] === T_SEMICOLON) { + /* This while is probably part of a do-while construction, skip it .. */ + return; + } + if ($tokens[$nextToken]['code'] !== T_OPEN_CURLY_BRACKET && $tokens[$nextToken]['code'] !== T_COLON) { + $error = 'Curly brackets required in a do-while or while loop'; + $phpcsFile->addError($error, $stackPtr, 'NotAllowed'); + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php new file mode 100644 index 000000000..7fe7e31cc --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Formatting/BlankLineBeforeReturnSniff.php @@ -0,0 +1,78 @@ + + * @license http://spdx.org/licenses/MIT MIT License + * @link https://github.com/escapestudios/Symfony2-coding-standard + */ +class BlankLineBeforeReturnSniff implements Sniff +{ + /** + * A list of tokenizers this sniff supports. + * + * @var array + */ + public $supportedTokenizers = [ + 'PHP', + 'JS', + ]; + + /** + * {@inheritDoc} + */ + public function register() + { + return [T_RETURN]; + } + + /** + * {@inheritDoc} + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $current = $stackPtr; + $previousLine = $tokens[$stackPtr]['line'] - 1; + $prevLineTokens = []; + + while ($current >= 0 && $tokens[$current]['line'] >= $previousLine) { + if ($tokens[$current]['line'] == $previousLine + && $tokens[$current]['type'] !== 'T_WHITESPACE' + && $tokens[$current]['type'] !== 'T_COMMENT' + && $tokens[$current]['type'] !== 'T_DOC_COMMENT_OPEN_TAG' + && $tokens[$current]['type'] !== 'T_DOC_COMMENT_TAG' + && $tokens[$current]['type'] !== 'T_DOC_COMMENT_STRING' + && $tokens[$current]['type'] !== 'T_DOC_COMMENT_CLOSE_TAG' + && $tokens[$current]['type'] !== 'T_DOC_COMMENT_WHITESPACE' + ) { + $prevLineTokens[] = $tokens[$current]['type']; + } + $current--; + } + + if (isset($prevLineTokens[0]) + && ($prevLineTokens[0] === 'T_OPEN_CURLY_BRACKET' + || $prevLineTokens[0] === 'T_COLON' + || $prevLineTokens[0] === 'T_OPEN_TAG') + ) { + return; + } elseif (count($prevLineTokens) > 0) { + $fix = $phpcsFile->addFixableError( + 'Missing blank line before return statement', + $stackPtr, + 'BlankLineBeforeReturn' + ); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addNewlineBefore($stackPtr - 1); + $phpcsFile->fixer->endChangeset(); + } + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Formatting/UseInAlphabeticalOrderSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Formatting/UseInAlphabeticalOrderSniff.php new file mode 100644 index 000000000..5e3020a9c --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Formatting/UseInAlphabeticalOrderSniff.php @@ -0,0 +1,144 @@ +_processed[$phpcsFile->getFilename()])) { + return; + } + $filename = $phpcsFile->getFilename(); + + $this->_uses = []; + $next = $stackPtr; + + while ($next !== false) { + $this->_checkUseToken($phpcsFile, $next); + $next = $phpcsFile->findNext(T_USE, $next + 1); + } + + // Prevent multiple uses in the same file from entering + $this->_processed[$phpcsFile->getFilename()] = true; + + foreach ($this->_uses as $scope => $used) { + $defined = $sorted = array_keys($used); + + natcasesort($sorted); + $sorted = array_values($sorted); + if ($sorted === $defined) { + continue; + } + + foreach ($defined as $i => $name) { + if ($name !== $sorted[$i]) { + $error = 'Use classes must be in alphabetical order. Was expecting ' . $sorted[$i]; + $phpcsFile->addError($error, $used[$name], 'UseInAlphabeticalOrder'); + } + } + } + } + + /** + * Check all the use tokens in a file. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file to check. + * @param int $stackPtr The index of the first use token. + * @return void + */ + protected function _checkUseToken($phpcsFile, $stackPtr) + { + // If the use token is for a closure we want to ignore it. + $isClosure = $this->_isClosure($phpcsFile, $stackPtr); + if ($isClosure) { + return; + } + + $tokens = $phpcsFile->getTokens(); + + $content = ''; + $end = $phpcsFile->findNext([T_SEMICOLON, T_OPEN_CURLY_BRACKET], $stackPtr); + $useTokens = array_slice($tokens, $stackPtr, $end - $stackPtr, true); + + foreach ($useTokens as $index => $token) { + if ($token['code'] === T_STRING || $token['code'] === T_NS_SEPARATOR) { + $content .= $token['content']; + } + } + + // Check for class scoping on use. Traits should be + // ordered independently. + $scope = 0; + if (!empty($token['conditions'])) { + $scope = key($token['conditions']); + } + $this->_uses[$scope][$content] = $stackPtr; + } + + /** + * Check if the current stackPtr is a use token that is for a closure. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the first use token. + * @return bool + */ + protected function _isClosure($phpcsFile, $stackPtr) + { + return $phpcsFile->findPrevious( + [T_CLOSURE], + ($stackPtr - 1), + null, + false, + null, + true + ); + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Functions/ClosureDeclarationSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Functions/ClosureDeclarationSniff.php new file mode 100644 index 000000000..eb0bbfa8d --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Functions/ClosureDeclarationSniff.php @@ -0,0 +1,54 @@ +getTokens(); + $spaces = 0; + + if ($tokens[($stackPtr + 1)]['code'] === T_WHITESPACE) { + $spaces = strlen($tokens[($stackPtr + 1)]['content']); + } + + if ($spaces !== 1) { + $error = 'Expected 1 space after closure\'s function keyword; %s found'; + $data = [$spaces]; + $phpcsFile->addError($error, $stackPtr, 'SpaceAfterFunction', $data); + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php new file mode 100644 index 000000000..e3f4ea50c --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php @@ -0,0 +1,34 @@ + + * @author Marc McIntyre + * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @link http://pear.php.net/package/PHP_CodeSniffer + */ + +/** + * CakePHP_Sniffs_NamingConventions_UpperCaseConstantNameSniff. + * + * Ensures that constant names are all uppercase. + * + * @category PHP + * @package PHP_CodeSniffer + * @author Greg Sherwood + * @author Marc McIntyre + * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @version Release: 1.5.0RC3 + * @link http://pear.php.net/package/PHP_CodeSniffer + */ +namespace CakePHP\Sniffs\NamingConventions; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; + +class UpperCaseConstantNameSniff implements Sniff +{ + + /** + * {@inheritDoc} + */ + public function register() + { + return [T_STRING]; + } + + /** + * {@inheritDoc} + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $constName = $tokens[$stackPtr]['content']; + + // If this token is in a heredoc, ignore it. + if ($phpcsFile->hasCondition($stackPtr, T_START_HEREDOC) === true) { + return; + } + + // Special case for PHPUnit. + if ($constName === 'PHPUnit_MAIN_METHOD') { + return; + } + + // If the next non-whitespace token after this token + // is not an opening parenthesis then it is not a function call. + $openBracket = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if ($tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS) { + $functionKeyword = $phpcsFile->findPrevious( + [ + T_WHITESPACE, + T_COMMA, + T_COMMENT, + T_STRING, + T_NS_SEPARATOR, + ], + ($stackPtr - 1), + null, + true + ); + + $declarations = [ + T_FUNCTION, + T_CLASS, + T_INTERFACE, + T_TRAIT, + T_IMPLEMENTS, + T_EXTENDS, + T_INSTANCEOF, + T_NEW, + T_NAMESPACE, + T_USE, + T_AS, + T_GOTO, + T_INSTEADOF, + T_PROTECTED, + T_PRIVATE, + T_PUBLIC, + ]; + + if (in_array($tokens[$functionKeyword]['code'], $declarations) === true) { + // This is just a declaration; no constants here. + return; + } + + if ($tokens[$functionKeyword]['code'] === T_CONST) { + // This is a class constant. + if (strtoupper($constName) !== $constName) { + $error = 'Class constants must be uppercase; expected %s but found %s'; + $data = [ + strtoupper($constName), + $constName, + ]; + $phpcsFile->addError($error, $stackPtr, 'ClassConstantNotUpperCase', $data); + } + + return; + } + + // Is this a class name? + $nextPtr = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if ($tokens[$nextPtr]['code'] === T_DOUBLE_COLON) { + return; + } + + // Is this a namespace name? + if ($tokens[$nextPtr]['code'] === T_NS_SEPARATOR) { + return; + } + + // Is this an insteadof name? + if ($tokens[$nextPtr]['code'] === T_INSTEADOF) { + return; + } + + // Is this an as name? + if ($tokens[$nextPtr]['code'] === T_AS) { + return; + } + + // Is this a type hint? + if ($tokens[$nextPtr]['code'] === T_VARIABLE + || $phpcsFile->isReference($nextPtr) === true + ) { + return; + } + + // Is this a member var name? + $prevPtr = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); + if ($tokens[$prevPtr]['code'] === T_OBJECT_OPERATOR) { + return; + } + + // Is this a variable name, in the form ${varname} ? + if ($tokens[$prevPtr]['code'] === T_OPEN_CURLY_BRACKET) { + $nextPtr = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if ($tokens[$nextPtr]['code'] === T_CLOSE_CURLY_BRACKET) { + return; + } + } + + // Is this a namespace name? + if ($tokens[$prevPtr]['code'] === T_NS_SEPARATOR) { + return; + } + + // Is this an instance of declare() + $prevPtrDeclare = $phpcsFile->findPrevious([T_WHITESPACE, T_OPEN_PARENTHESIS], ($stackPtr - 1), null, true); + if ($tokens[$prevPtrDeclare]['code'] === T_DECLARE) { + return; + } + + // Is this a goto label target? + if ($tokens[$nextPtr]['code'] === T_COLON) { + if (in_array($tokens[$prevPtr]['code'], [T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_COLON], true)) { + return; + } + } + + // This is a real constant. Ignore ::class from php5.5 + if (strtoupper($constName) !== $constName && $constName !== 'class') { + $error = 'Constants must be uppercase; expected %s but found %s'; + $data = [ + strtoupper($constName), + $constName, + ]; + $phpcsFile->addError($error, $stackPtr, 'ConstantNotUpperCase', $data); + } + } elseif (strtolower($constName) === 'define' || strtolower($constName) === 'constant') { + // This may be a "define" or "constant" function call. + + // Make sure this is not a method call. + $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); + if ($tokens[$prev]['code'] === T_OBJECT_OPERATOR + || $tokens[$prev]['code'] === T_DOUBLE_COLON + ) { + return; + } + + // The next non-whitespace token must be the constant name. + $constPtr = $phpcsFile->findNext(T_WHITESPACE, ($openBracket + 1), null, true); + if ($tokens[$constPtr]['code'] !== T_CONSTANT_ENCAPSED_STRING) { + return; + } + + $constName = $tokens[$constPtr]['content']; + + // Check for constants like self::CONSTANT. + $prefix = ''; + $splitPos = strpos($constName, '::'); + if ($splitPos !== false) { + $prefix = substr($constName, 0, ($splitPos + 2)); + $constName = substr($constName, ($splitPos + 2)); + } + + if (strtoupper($constName) !== $constName) { + $error = 'Constants must be uppercase; expected %s but found %s'; + $data = [ + $prefix . strtoupper($constName), + $prefix . $constName, + ]; + $phpcsFile->addError($error, $stackPtr, 'ConstantNotUpperCase', $data); + } + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php new file mode 100644 index 000000000..3b2352c52 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/NamingConventions/ValidFunctionNameSniff.php @@ -0,0 +1,98 @@ +getDeclarationName($stackPtr); + $className = $phpcsFile->getDeclarationName($currScope); + $errorData = [$className . '::' . $methodName]; + + // Ignore magic methods + if (preg_match('/^__(' . implode('|', $this->_magicMethods) . ')$/', $methodName)) { + return; + } + + $methodProps = $phpcsFile->getMethodProperties($stackPtr); + if ($methodProps['scope_specified'] === false) { + // Let another sniffer take care of that + return; + } + + $isPublic = $methodProps['scope'] === 'public'; + + if ($isPublic === true && $methodName[0] === '_') { + $error = 'Public method name "%s" must not be prefixed with underscore'; + $phpcsFile->addError($error, $stackPtr, 'PublicWithUnderscore', $errorData); + + return; + } + } + + /** + * {@inheritDoc} + */ + protected function processTokenOutsideScope(File $phpcsFile, $stackPtr) + { + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/NamingConventions/ValidTraitNameSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/NamingConventions/ValidTraitNameSniff.php new file mode 100644 index 000000000..5c9acb420 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/NamingConventions/ValidTraitNameSniff.php @@ -0,0 +1,51 @@ +getTokens(); + $traitName = $tokens[$stackPtr + 2]['content']; + + if (substr($traitName, -5) !== 'Trait') { + $error = 'Traits must have a "Trait" suffix.'; + $phpcsFile->addError($error, $stackPtr, 'InvalidTraitName'); + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/PHP/DisallowShortOpenTagSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/PHP/DisallowShortOpenTagSniff.php new file mode 100644 index 000000000..eaf65480a --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/PHP/DisallowShortOpenTagSniff.php @@ -0,0 +1,62 @@ +getTokens(); + $openTag = $tokens[$stackPtr]; + + if (trim($openTag['content']) === 'addError($error, $stackPtr, 'Found', $data); + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/PHP/TypeCastingSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/PHP/TypeCastingSniff.php new file mode 100644 index 000000000..0ccae90ea --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/PHP/TypeCastingSniff.php @@ -0,0 +1,86 @@ +getTokens(); + + // Process !! casts + if ($tokens[$stackPtr]['code'] == T_BOOLEAN_NOT) { + $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if (!$nextToken) { + return; + } + if ($tokens[$nextToken]['code'] != T_BOOLEAN_NOT) { + return; + } + $error = 'Usage of !! cast is not allowed. Please use (bool) to cast.'; + $phpcsFile->addError($error, $stackPtr, 'NotAllowed'); + + return; + } + + // Only allow short forms if both short and long forms are possible + $matching = [ + '(boolean)' => '(bool)', + '(integer)' => '(int)', + ]; + $content = $tokens[$stackPtr]['content']; + $key = strtolower($content); + if (isset($matching[$key])) { + $error = 'Please use ' . $matching[$key] . ' instead of ' . $content . '.'; + $phpcsFile->addError($error, $stackPtr, 'NotAllowed'); + + return; + } + if ($content !== $key) { + $error = 'Please use ' . $key . ' instead of ' . $content . '.'; + $phpcsFile->addError($error, $stackPtr, 'NotAllowed'); + + return; + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Strings/ConcatenationSpacingSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Strings/ConcatenationSpacingSniff.php new file mode 100644 index 000000000..05b544d83 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/Strings/ConcatenationSpacingSniff.php @@ -0,0 +1,71 @@ +getTokens(); + if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE) { + $message = 'Expected 1 space before ., but 0 found'; + $phpcsFile->addError($message, $stackPtr, 'MissingBefore'); + } else { + $content = str_replace("\r\n", "\n", $tokens[($stackPtr - 1)]['content']); + $spaces = strlen($content); + if ($spaces > 1) { + $message = 'Expected 1 space before ., but %d found'; + $data = [$spaces]; + $phpcsFile->addError($message, $stackPtr, 'TooManyBefore', $data); + } + } + + if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) { + $message = 'Expected 1 space after ., but 0 found'; + $phpcsFile->addError($message, $stackPtr, 'MissingAfter'); + } else { + $content = str_replace("\r\n", "\n", $tokens[($stackPtr + 1)]['content']); + $spaces = strlen($content); + if ($spaces > 1) { + $message = 'Expected 1 space after ., but %d found'; + $data = [$spaces]; + $phpcsFile->addError($message, $stackPtr, 'TooManyAfter', $data); + } + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/CommaSpacingSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/CommaSpacingSniff.php new file mode 100644 index 000000000..7f96cda7c --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/CommaSpacingSniff.php @@ -0,0 +1,72 @@ +getTokens(); + + $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + + if ($tokens[$next]['code'] !== T_WHITESPACE && ($next !== $stackPtr + 2)) { + // Skip if immediate char is comma + if ($tokens[$next]['code'] === T_COMMA) { + return; + } + + // Last character in a line is ok. + if ($tokens[$next]['line'] === $tokens[$stackPtr]['line']) { + $error = 'Missing space after comma'; + $fix = $phpcsFile->addFixableError($error, $next, 'MissingSpaceAfterComma'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addContent($stackPtr, ' '); + $phpcsFile->fixer->endChangeset(); + } + } + } + + $previous = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); + + if ($tokens[$previous]['code'] !== T_WHITESPACE && ($previous !== $stackPtr - 1)) { + $error = 'Space before comma, expected none, though'; + $phpcsFile->addError($error, $next, 'SpaceBeforeComma'); + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/EmptyLinesSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/EmptyLinesSniff.php new file mode 100644 index 000000000..c2eaf78d7 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/EmptyLinesSniff.php @@ -0,0 +1,59 @@ +getTokens(); + if ($tokens[$stackPtr]['content'] === $phpcsFile->eolChar + && isset($tokens[($stackPtr + 1)]) === true + && $tokens[($stackPtr + 1)]['content'] === $phpcsFile->eolChar + && isset($tokens[($stackPtr + 2)]) === true + && $tokens[($stackPtr + 2)]['content'] === $phpcsFile->eolChar + ) { + $error = 'Found more than a single empty line between content'; + $fix = $phpcsFile->addFixableError($error, ($stackPtr + 3), 'EmptyLines'); + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr + 2, ''); + } + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionCallSpacingSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionCallSpacingSniff.php new file mode 100644 index 000000000..0d89cf78e --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionCallSpacingSniff.php @@ -0,0 +1,68 @@ +getTokens(); + + // Find the next non-empty token. + $openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + + if ($tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS) { + // Not a function call. + return; + } + + // Look for funcName ( + if (($stackPtr + 1) !== $openBracket) { + $error = 'Space before opening parenthesis of function call not allowed'; + $phpcsFile->addError($error, $stackPtr, 'SpaceBeforeOpenBracket'); + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionClosingBraceSpaceSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionClosingBraceSpaceSniff.php new file mode 100644 index 000000000..1a97f5d88 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionClosingBraceSpaceSniff.php @@ -0,0 +1,74 @@ +getTokens(); + + if (isset($tokens[$stackPtr]['scope_closer']) === false) { + // Probably an interface method. + return; + } + + $closeBrace = $tokens[$stackPtr]['scope_closer']; + $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($closeBrace - 1), null, true); + + $braceLine = $tokens[$closeBrace]['line']; + $prevLine = $tokens[$prevContent]['line']; + + $found = ($braceLine - $prevLine - 1); + if ($phpcsFile->hasCondition($stackPtr, T_FUNCTION) === true || isset($tokens[$stackPtr]['nested_parenthesis']) === true) { + // Nested function. + if ($found < 0) { + $error = 'Closing brace of nested function must be on a new line'; + $phpcsFile->addError($error, $closeBrace, 'ContentBeforeClose'); + } elseif ($found > 0) { + $error = 'Expected 0 blank lines before closing brace of nested function; %s found'; + $data = [$found]; + $phpcsFile->addError($error, $closeBrace, 'SpacingBeforeNestedClose', $data); + } + } else { + if ($found !== 0) { + $error = 'Expected 0 blank lines before closing function brace; %s found'; + $data = [$found]; + $phpcsFile->addError($error, $closeBrace, 'SpacingBeforeClose', $data); + } + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionOpeningBraceSpaceSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionOpeningBraceSpaceSniff.php new file mode 100644 index 000000000..d0d54c0f7 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionOpeningBraceSpaceSniff.php @@ -0,0 +1,68 @@ +getTokens(); + + if (isset($tokens[$stackPtr]['scope_opener']) === false) { + // Probably an interface method. + return; + } + + $openBrace = $tokens[$stackPtr]['scope_opener']; + $nextContent = $phpcsFile->findNext(T_WHITESPACE, ($openBrace + 1), null, true); + + if ($nextContent === $tokens[$stackPtr]['scope_closer']) { + // The next bit of content is the closing brace, so this + // is an empty function and should have a blank line + // between the opening and closing braces. + return; + } + + $braceLine = $tokens[$openBrace]['line']; + $nextLine = $tokens[$nextContent]['line']; + + $found = ($nextLine - $braceLine - 1); + if ($found > 0) { + $error = 'Expected 0 blank lines after opening function brace; %s found'; + $data = [$found]; + $phpcsFile->addError($error, $openBrace, 'SpacingAfter', $data); + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php new file mode 100644 index 000000000..f1901b029 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/FunctionSpacingSniff.php @@ -0,0 +1,135 @@ +getTokens(); + + /* + Check the number of blank lines + after the function. + */ + if (isset($tokens[$stackPtr]['scope_closer']) === false) { + // Must be an interface method, so the closer is the semi-colon. + $closer = $phpcsFile->findNext(T_SEMICOLON, $stackPtr); + } else { + $closer = $tokens[$stackPtr]['scope_closer']; + } + + // There needs to be 1 blank lines after the closer. + $nextLineToken = null; + for ($i = $closer; $i < $phpcsFile->numTokens; $i++) { + if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) === false) { + continue; + } else { + $nextLineToken = ($i + 1); + break; + } + } + + if ($nextLineToken === null) { + // Never found the next line, which means + // there are 0 blank lines after the function. + $foundLines = 0; + } else { + $nextContent = $phpcsFile->findNext([T_WHITESPACE], ($nextLineToken + 1), null, true); + if ($nextContent === false) { + // We are at the end of the file. That is acceptable as well. + $foundLines = 1; + } else { + $foundLines = ($tokens[$nextContent]['line'] - $tokens[$nextLineToken]['line']); + } + } + + /* + Check the number of blank lines + before the function. + */ + + $prevLineToken = null; + for ($i = $stackPtr; $i > 0; $i--) { + if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) === false) { + continue; + } else { + $prevLineToken = $i; + break; + } + } + + if ($prevLineToken === null) { + // Never found the previous line, which means + // there are 0 blank lines before the function. + $foundLines = 0; + } else { + $prevContent = $phpcsFile->findPrevious([T_WHITESPACE, T_DOC_COMMENT], $prevLineToken, null, true); + + // Before we throw an error, check that we are not throwing an error + // for another function. We don't want to error for no blank lines after + // the previous function and no blank lines before this one as well. + $currentLine = $tokens[$stackPtr]['line']; + $prevLine = ($tokens[$prevContent]['line'] - 1); + $i = ($stackPtr - 1); + $foundLines = 0; + while ($currentLine !== $prevLine && $currentLine > 1 && $i > 0) { + if (isset($tokens[$i]['scope_condition']) === true) { + $scopeCondition = $tokens[$i]['scope_condition']; + if ($tokens[$scopeCondition]['code'] === T_FUNCTION) { + // Found a previous function. + return; + } + } elseif ($tokens[$i]['code'] === T_FUNCTION) { + // Found another interface function. + return; + } + + $currentLine = $tokens[$i]['line']; + if ($currentLine === $prevLine) { + break; + } + + if ($tokens[($i - 1)]['line'] < $currentLine && $tokens[($i + 1)]['line'] > $currentLine) { + // This token is on a line by itself. If it is whitespace, the line is empty. + if ($tokens[$i]['code'] === T_WHITESPACE) { + $foundLines++; + } + } + $i--; + } + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php new file mode 100644 index 000000000..c14a16377 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php @@ -0,0 +1,50 @@ +getTokens(); + + $nextType = $tokens[($stackPtr + 1)]['code']; + if (in_array($nextType, Tokens::$emptyTokens) === true) { + $error = 'Space found after object operator'; + $phpcsFile->addError($error, $stackPtr, 'After'); + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/OperatorSpacingSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/OperatorSpacingSniff.php new file mode 100644 index 000000000..d6b20689f --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/OperatorSpacingSniff.php @@ -0,0 +1,203 @@ +getTokens(); + + // Skip default values in function declarations. + if ($tokens[$stackPtr]['code'] === T_EQUAL + || $tokens[$stackPtr]['code'] === T_MINUS + ) { + if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) { + $parenthesis = array_keys($tokens[$stackPtr]['nested_parenthesis']); + $bracket = array_pop($parenthesis); + if (isset($tokens[$bracket]['parenthesis_owner']) === true) { + $function = $tokens[$bracket]['parenthesis_owner']; + if ($tokens[$function]['code'] === T_FUNCTION) { + return; + } + } + } + } + + if ($tokens[$stackPtr]['code'] === T_EQUAL) { + // Skip for '=&' case. + if (isset($tokens[($stackPtr + 1)]) === true && $tokens[($stackPtr + 1)]['code'] === T_BITWISE_AND) { + return; + } + } + + if ($tokens[$stackPtr]['code'] === T_BITWISE_AND) { + // If its not a reference, then we expect one space either side of the + // bitwise operator. + if (!$phpcsFile->isReference($stackPtr) && !$this->_isVariable($stackPtr, $tokens, $phpcsFile)) { + // Check there is one space before the & operator. + if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE) { + $error = 'Expected 1 space before "&" operator; 0 found'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceBeforeAmp'); + if ($fix === true) { + $phpcsFile->fixer->addContentBefore($stackPtr, ' '); + } + } + + // Check there is one space after the & operator. + if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) { + $error = 'Expected 1 space after "&" operator; 0 found'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceAfterAmp'); + if ($fix === true) { + $phpcsFile->fixer->addContent($stackPtr, ' '); + } + } + } + } else { + if ($tokens[$stackPtr]['code'] === T_MINUS) { + // Skip declaration of negative value in new array format; eg. $arr = [-1]. + if ($tokens[($stackPtr - 1)]['code'] === T_OPEN_SHORT_ARRAY) { + return; + } + + // Check minus spacing, but make sure we aren't just assigning + // a minus value or returning one. + $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); + if ($tokens[$prev]['code'] === T_RETURN) { + // Just returning a negative value; eg. return -1. + return; + } + + if (in_array($tokens[$prev]['code'], Tokens::$operators) === true) { + // Just trying to operate on a negative value; eg. ($var * -1). + return; + } + + if (in_array($tokens[$prev]['code'], Tokens::$comparisonTokens) === true) { + // Just trying to compare a negative value; eg. ($var === -1). + return; + } + + // A list of tokens that indicate that the token is not + // part of an arithmetic operation. + $invalidTokens = [ + T_COMMA, + T_OPEN_PARENTHESIS, + T_OPEN_SQUARE_BRACKET, + T_DOUBLE_ARROW, + T_COLON, + T_INLINE_THEN, + T_INLINE_ELSE, + T_CASE, + ]; + + if (in_array($tokens[$prev]['code'], $invalidTokens) === true) { + // Just trying to use a negative value; eg. myFunction($var, -2). + return; + } + if (in_array($tokens[$prev]['code'], Tokens::$assignmentTokens) === true) { + // Just trying to assign a negative value; eg. ($var = -1). + return; + } + } + + $operator = $tokens[$stackPtr]['content']; + + if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE) { + $error = "Expected 1 space before \"$operator\"; 0 found"; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceBefore'); + if ($fix === true) { + $phpcsFile->fixer->addContentBefore($stackPtr, ' '); + } + } + + if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) { + $error = "Expected 1 space after \"$operator\"; 0 found"; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceAfter'); + if ($fix === true) { + $phpcsFile->fixer->addContent($stackPtr, ' '); + } + } + } + } + + /** + * Check if the current token is inside an array. + * + * @param int $stackPtr The current token offset. + * @param array $tokens The current token list. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being checked. + * @return bool + */ + protected function _isVariable($stackPtr, $tokens, $phpcsFile) + { + $tokenAfter = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($stackPtr + 1), + null, + true + ); + $tokenBefore = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($stackPtr - 1), + null, + true + ); + + return ($tokens[$tokenAfter]['code'] === T_VARIABLE && + ( + $tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS || + $tokens[$tokenBefore]['code'] === T_COMMA || + $tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY + ) + ); + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/TabAndSpaceSniff.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/TabAndSpaceSniff.php new file mode 100644 index 000000000..e9d14cbf7 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Sniffs/WhiteSpace/TabAndSpaceSniff.php @@ -0,0 +1,75 @@ +getTokens(); + + $line = $tokens[$stackPtr]['line']; + if ($stackPtr > 0 && $tokens[($stackPtr - 1)]['line'] !== $line) { + return; + } + + if (strpos($tokens[$stackPtr]['content'], ' ') !== false) { + $error = 'Double space found'; + $phpcsFile->addError($error, $stackPtr, 'DoubleSpace'); + } + if (strpos($tokens[$stackPtr]['content'], " \t") !== false) { + $error = 'Space and tab found'; + $phpcsFile->addError($error, $stackPtr, 'SpaceAndTab'); + } + if (strpos($tokens[$stackPtr]['content'], "\t ") !== false) { + $error = 'Tab and space found'; + $phpcsFile->addError($error, $stackPtr, 'TabAndSpace'); + } + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Commenting/DocBlockAlignmentUnitTest.1.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Commenting/DocBlockAlignmentUnitTest.1.inc new file mode 100644 index 000000000..38bf963be --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Commenting/DocBlockAlignmentUnitTest.1.inc @@ -0,0 +1,34 @@ + 1, + 7 => 1, + 14 => 1, + 21 => 1, + 30 => 1 + ]; + + default: + return []; + } + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Commenting/FunctionCommentUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Commenting/FunctionCommentUnitTest.inc new file mode 100644 index 000000000..4225b82ab --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Commenting/FunctionCommentUnitTest.inc @@ -0,0 +1,251 @@ + 1, + 13 => 1, + 23 => 1, + 24 => 1, + 34 => 1, + 35 => 1, + 90 => 1, + 97 => 1, + 104 => 1, + 112 => 1, + 222 => 1, + 231 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return [ + 14 => 1, + 31 => 2, + 45 => 1, + 140 => 1, + 145 => 1, + 155 => 1, + 165 => 1, + 174 => 1, + 182 => 1, + 190 => 1, + 197 => 1, + 198 => 1, + 205 => 1, + 206 => 1, + 215 => 1, + 221 => 1, + ]; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/ControlStructuresUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/ControlStructuresUnitTest.inc new file mode 100644 index 000000000..b51457787 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/ControlStructuresUnitTest.inc @@ -0,0 +1,14 @@ + 100) { + echo 'i > 100'; +} + +if($abc == true) + echo 'hello'; diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/ControlStructuresUnitTest.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/ControlStructuresUnitTest.php new file mode 100644 index 000000000..1e1cc02bd --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/ControlStructuresUnitTest.php @@ -0,0 +1,26 @@ + 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/ElseIfDeclarationUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/ElseIfDeclarationUnitTest.inc new file mode 100644 index 000000000..aa97c9e14 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/ElseIfDeclarationUnitTest.inc @@ -0,0 +1,6 @@ + 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/WhileStructuresUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/WhileStructuresUnitTest.inc new file mode 100644 index 000000000..79b57dae3 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/ControlStructures/WhileStructuresUnitTest.inc @@ -0,0 +1,16 @@ + 1, + 5 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/BlankLineBeforeReturnUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/BlankLineBeforeReturnUnitTest.inc new file mode 100644 index 000000000..c5c3725c5 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/BlankLineBeforeReturnUnitTest.inc @@ -0,0 +1,104 @@ + 'bar' +]; + +function invalidFunctionReturnOne() +{ + echo ""; + return null; +} + +function invalidFunctionReturnOne() +{ + if(true) { + echo ""; + } + return null; +} + +/** + * [validFunctionReturnOne description] + * + * @return null + */ +function validFunctionReturnOne() +{ + return null; +} + +/** + * [validFunctionReturnTwo description] + * + * @return null|bool + */ +function validFunctionReturnTwo() +{ + if ($a) { + return true; + } + + return null; +} + +/** + * [validFunctionReturnThree description] + * + * @return null + */ +function validFunctionReturnThree() +{ + echo ""; + + return null; +} + +/** + * [validFunctionReturnFour description] + * + * @return null + */ +function validFunctionReturnFour() +{ + // comment + return null; +} + +/** + * [validFunctionReturnFive description] + * + * @return null + */ +function validFunctionReturnFive() +{ + /** + * multi-line + */ + return null; +} + +/** + * [validFunctionReturnSix description] + * + * @return null|bool + */ +function validFunctionReturnSix() +{ + switch ($condition) { + case 'foo': + return true; + default: + return false; + } +} + +/** + * [validFunctionReturnSeven description] + * + * @return null + */ +function validFunctionReturnSeven() +{ + /** @var Foo $foo */ + return null; +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/BlankLineBeforeReturnUnitTest.inc.fixed b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/BlankLineBeforeReturnUnitTest.inc.fixed new file mode 100644 index 000000000..cb38504f4 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/BlankLineBeforeReturnUnitTest.inc.fixed @@ -0,0 +1,106 @@ + 'bar' +]; + +function invalidFunctionReturnOne() +{ + echo ""; + + return null; +} + +function invalidFunctionReturnOne() +{ + if(true) { + echo ""; + } + + return null; +} + +/** + * [validFunctionReturnOne description] + * + * @return null + */ +function validFunctionReturnOne() +{ + return null; +} + +/** + * [validFunctionReturnTwo description] + * + * @return null|bool + */ +function validFunctionReturnTwo() +{ + if ($a) { + return true; + } + + return null; +} + +/** + * [validFunctionReturnThree description] + * + * @return null + */ +function validFunctionReturnThree() +{ + echo ""; + + return null; +} + +/** + * [validFunctionReturnFour description] + * + * @return null + */ +function validFunctionReturnFour() +{ + // comment + return null; +} + +/** + * [validFunctionReturnFive description] + * + * @return null + */ +function validFunctionReturnFive() +{ + /** + * multi-line + */ + return null; +} + +/** + * [validFunctionReturnSix description] + * + * @return null|bool + */ +function validFunctionReturnSix() +{ + switch ($condition) { + case 'foo': + return true; + default: + return false; + } +} + +/** + * [validFunctionReturnSeven description] + * + * @return null + */ +function validFunctionReturnSeven() +{ + /** @var Foo $foo */ + return null; +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/BlankLineBeforeReturnUnitTest.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/BlankLineBeforeReturnUnitTest.php new file mode 100644 index 000000000..42a87ed16 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/BlankLineBeforeReturnUnitTest.php @@ -0,0 +1,27 @@ + 1, + 17 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/UseInAlphabeticalOrderUnitTest.1.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/UseInAlphabeticalOrderUnitTest.1.inc new file mode 100644 index 000000000..99780ef56 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Formatting/UseInAlphabeticalOrderUnitTest.1.inc @@ -0,0 +1,10 @@ + 1, + 4 => 1, + 8 => 1, + 9 => 1, + ]; + + default: + return []; + } + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.inc new file mode 100644 index 000000000..013c2f015 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Functions/ClosureDeclarationUnitTest.inc @@ -0,0 +1,36 @@ + 1, + 30 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Functions/FunctionDeclarationArgumentSpacingUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Functions/FunctionDeclarationArgumentSpacingUnitTest.inc new file mode 100644 index 000000000..82a3e7a0d --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Functions/FunctionDeclarationArgumentSpacingUnitTest.inc @@ -0,0 +1,21 @@ + 1, + 4 => 2, + 5 => 2, + 6 => 2, + 7 => 2, + 8 => 2, + 9 => 9, + 10 => 6, + 11 => 6, + 12 => 4, + 13 => 4, + 14 => 2, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/UpperCaseConstantNameUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/UpperCaseConstantNameUnitTest.inc new file mode 100644 index 000000000..c50b175f9 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/UpperCaseConstantNameUnitTest.inc @@ -0,0 +1,37 @@ + 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.inc new file mode 100644 index 000000000..e4bc5652d --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.inc @@ -0,0 +1,177 @@ +passingPublic = 'changed'; + $this->underscored = 'has value now'; + $this->doubleUnderscore = 'not recommended'; + } + + public static function setStatics() + { + self::$publicStatic = true; + self::$protectedStatic = true; + self::$privateStatic = true; + } + + protected function _someFunc() + { + // code here + } + + protected function noUnderscorePrefix() + { + // code here + $closure = function () { + // code here + }; + } + + public function __call($name, $arguments) + { + } + public function __construct() + { + } + public function __clone() + { + } + public function __debugInfo() + { + } + public function __destruct() + { + } + public function __get($name) + { + } + public function __invoke() + { + } + public function __isset($name) + { + } + public function __set($name, $value) + { + } + public function __sleep() + { + } + public function __toString() + { + return ''; + } + public function __unset($name) + { + } + public function __wakeup() + { + } +} + +interface FunctionNamesInterface +{ + public function _forbidden(); + + public function setVariables(); + + public static function setStatics(); +} + +trait FunctionNamesTrait +{ + public function _forbidden() + { + echo "I emit an error"; + } + + private function notDiscouraged() + { + echo "I don't emit a warning anymore"; + } + + public function setVariables() + { + $this->passingPublic = 'changed'; + $this->underscored = 'has value now'; + $this->doubleUnderscore = 'not recommended'; + } + + public static function setStatics() + { + self::$publicStatic = true; + self::$protectedStatic = true; + self::$privateStatic = true; + } + + protected function _someFunc() + { + // code here + } + + protected function noUnderscorePrefix() + { + // code here + } + + function _noScopeSpecified() + { + echo 'handled by an other Sniff'; + } + + public function __call($name, $arguments) + { + } + public function __construct() + { + } + public function __clone() + { + } + public function __debugInfo() + { + } + public function __destruct() + { + } + public function __get($name) + { + } + public function __invoke() + { + } + public function __isset($name) + { + } + public function __set($name, $value) + { + } + public function __sleep() + { + } + public function __toString() + { + return ''; + } + public function __unset($name) + { + } + public function __wakeup() + { + } +} + +public function __passingOutside() {} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.php new file mode 100644 index 000000000..5312675f4 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/ValidFunctionNameUnitTest.php @@ -0,0 +1,28 @@ + 1, + 87 => 1, + 96 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/ValidTraitNameUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/ValidTraitNameUnitTest.inc new file mode 100644 index 000000000..57a83ea23 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/NamingConventions/ValidTraitNameUnitTest.inc @@ -0,0 +1,6 @@ + 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/PHP/DisallowShortOpenTagUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/PHP/DisallowShortOpenTagUnitTest.inc new file mode 100644 index 000000000..97995d335 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/PHP/DisallowShortOpenTagUnitTest.inc @@ -0,0 +1,9 @@ + + + 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/PHP/TypeCastingUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/PHP/TypeCastingUnitTest.inc new file mode 100644 index 000000000..d1c47b389 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/PHP/TypeCastingUnitTest.inc @@ -0,0 +1,11 @@ + 1, + 4 => 1, + 5 => 1, + 6 => 1, + 7 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Strings/ConcatenationSpacingUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Strings/ConcatenationSpacingUnitTest.inc new file mode 100644 index 000000000..44cc71658 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/Strings/ConcatenationSpacingUnitTest.inc @@ -0,0 +1,10 @@ + 1, + 5 => 1, + 6 => 2, + 7 => 1, + 8 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/CommaSpacingUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/CommaSpacingUnitTest.inc new file mode 100644 index 000000000..a4c9adbab --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/CommaSpacingUnitTest.inc @@ -0,0 +1,5 @@ + 1, + 3 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/EmptyLinesUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/EmptyLinesUnitTest.inc new file mode 100644 index 000000000..fa63e81f4 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/EmptyLinesUnitTest.inc @@ -0,0 +1,45 @@ + 1, + 22 => 1, + 43 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionCallSpacingUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionCallSpacingUnitTest.inc new file mode 100644 index 000000000..57070a382 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionCallSpacingUnitTest.inc @@ -0,0 +1,8 @@ +something ('testing'); +fail_whale (); diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionCallSpacingUnitTest.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionCallSpacingUnitTest.php new file mode 100644 index 000000000..d98f76dba --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionCallSpacingUnitTest.php @@ -0,0 +1,32 @@ + 1, + 3 => 1, + 4 => 1, + 5 => 1, + 6 => 1, + 7 => 1, + 8 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionClosingBraceSpaceUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionClosingBraceSpaceUnitTest.inc new file mode 100644 index 000000000..fc76fc8d0 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionClosingBraceSpaceUnitTest.inc @@ -0,0 +1,31 @@ + 1, + 6 => 1, + 10 => 1, + 25 => 1, + 28 => 1, + 30 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionOpeningBraceSpaceUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionOpeningBraceSpaceUnitTest.inc new file mode 100644 index 000000000..6e81d9f25 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/FunctionOpeningBraceSpaceUnitTest.inc @@ -0,0 +1,10 @@ + 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc new file mode 100644 index 000000000..0dc8f6aab --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc @@ -0,0 +1,4 @@ + failling() ; +$test->passing() ; diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.php b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.php new file mode 100644 index 000000000..513bbbc65 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.php @@ -0,0 +1,26 @@ + 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/OperatorSpacingUnitTest.inc b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/OperatorSpacingUnitTest.inc new file mode 100644 index 000000000..edc4886bf --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/OperatorSpacingUnitTest.inc @@ -0,0 +1,33 @@ + 1, + 3 => 1, + 4 => 2, + 5 => 1, + 6 => 1, + 7 => 2, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/OperatorSpacingunitTest.inc.fixed b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/OperatorSpacingunitTest.inc.fixed new file mode 100644 index 000000000..cbe2c5c99 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/Tests/WhiteSpace/OperatorSpacingunitTest.inc.fixed @@ -0,0 +1,33 @@ + 1, + 3 => 1, + ]; + } + + /** + * {@inheritDoc} + */ + public function getWarningList() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/CakePHP/ruleset.xml b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/ruleset.xml new file mode 100644 index 000000000..4f0093ae0 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/CakePHP/ruleset.xml @@ -0,0 +1,150 @@ + + + CakePHP coding standard + + \.git + */Config/*.ini.php + /*/tmp/ + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + */config/* + */tests/* + + + */tests/* + + + */tests/* + + + */CakePHP/* + */tests/* + + + */tests/* + + + */tests/* + + + */src/* + */tests/* + + + */src/* + */tests/* + + + */src/* + */tests/* + + + + diff --git a/app/vendor/cakephp/cakephp-codesniffer/LICENSE.txt b/app/vendor/cakephp/cakephp-codesniffer/LICENSE.txt new file mode 100644 index 000000000..414ab1e72 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/LICENSE.txt @@ -0,0 +1,28 @@ +The MIT License + +CakePHP(tm) : The Rapid Development PHP Framework (http://cakephp.org) +Copyright (c) 2005-2013, Cake Software Foundation, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +Cake Software Foundation, Inc. +1785 E. Sahara Avenue, +Suite 490-204 +Las Vegas, Nevada 89104, +United States of America. \ No newline at end of file diff --git a/app/vendor/cakephp/cakephp-codesniffer/README.md b/app/vendor/cakephp/cakephp-codesniffer/README.md new file mode 100644 index 000000000..eb1ee9c6c --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/README.md @@ -0,0 +1,50 @@ +# CakePHP Code Sniffer [![Build Status](https://travis-ci.org/cakephp/cakephp-codesniffer.png?branch=master)](http://travis-ci.org/cakephp/cakephp-codesniffer) + +This code works with [phpcs](http://pear.php.net/manual/en/package.php.php-codesniffer.php) +and checks code against the coding standards used in CakePHP. + +:warning: The `master` branch contains codesniffer rules that are based on the +PSR2 standard. If you want to check against the historical CakePHP coding +standard use any of the `1.x` releases. + +## Installation + +You should install this codesniffer with composer: + + composer require --dev "cakephp/cakephp-codesniffer" + vendor/bin/phpcs --config-set installed_paths /path/to/your/app/vendor/cakephp/cakephp-codesniffer + +The second command lets `phpcs` know where to find your new sniffs. Ensure that +you do not overwrite any existing `installed_paths` value. + +## Usage + +Depending on how you installed the code sniffer changes how you run it. If you have +installed phpcs, and this package with PEAR, you can do the following: + + vendor/bin/phpcs --standard=CakePHP /path/to/code + +:warning: Warning when these sniffs are installed with composer, ensure that +you have configured the CodeSniffer `installed_paths` setting. + +## Running Tests + +You can run tests with composer. Because of how PHPCS test suites work, there is +additional configuration state in `phpcs` that is required. + +```bash +composer test +``` + +Once this has been done once, you can use `phpunit --filter CakePHP` to run the +tests for the rules in this repository. + +## Contributing + +If you'd like to contribute to the Code Sniffer, you can fork the project add +features and send pull requests. + +## Releasing CakePHP Code Sniffer + +* Create a signed tag +* Write the changelog in the tag commit diff --git a/app/vendor/cakephp/cakephp-codesniffer/composer.json b/app/vendor/cakephp/cakephp-codesniffer/composer.json new file mode 100644 index 000000000..ca4706569 --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/composer.json @@ -0,0 +1,54 @@ +{ + "name": "cakephp/cakephp-codesniffer", + "description": "CakePHP CodeSniffer Standards", + "type": "phpcodesniffer-standard", + "keywords": ["framework", "codesniffer"], + "homepage": "http://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/cakephp-codesniffer/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp-codesniffer/issues", + "forum": "http://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/cakephp-codesniffer" + }, + "require": { + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.0.0" + }, + "require-dev": { + "phpunit/phpunit": "<6.0" + }, + "autoload": { + "psr-4": { + "CakePHP\\": "CakePHP" + } + }, + "scripts": { + "increase-severity": "sed -i.bak 's/0<\\/severity>//' CakePHP/ruleset.xml", + "reset-ruleset": [ + "sed -i.bak 's//0<\\/severity>/' CakePHP/ruleset.xml", + "rm -f CakePHP/ruleset.xml.bak" + ], + "add-standard" : "phpcs --config-set installed_paths $(pwd)", + "test": [ + "@add-standard", + "@increase-severity", + "phpunit", + "@reset-ruleset" + ], + "test-coverage": [ + "@add-standard", + "@increase-severity", + "phpunit --coverage-clover=clover.xml", + "@reset-ruleset" + ], + "cs-check": "phpcs --colors -p --extensions=php --standard=CakePHP ./CakePHP", + "cs-fix": "phpcbf --colors --extensions=php --standard=CakePHP ./CakePHP" + } +} diff --git a/app/vendor/cakephp/cakephp-codesniffer/phpunit.xml b/app/vendor/cakephp/cakephp-codesniffer/phpunit.xml new file mode 100644 index 000000000..087d813fc --- /dev/null +++ b/app/vendor/cakephp/cakephp-codesniffer/phpunit.xml @@ -0,0 +1,15 @@ + + + + + ./vendor/squizlabs/php_codesniffer/tests/AllTests.php + + + + + + + ./CakePHP/Sniffs + + + diff --git a/app/vendor/cakephp/cakephp/.mailmap b/app/vendor/cakephp/cakephp/.mailmap new file mode 100644 index 000000000..51b51af9d --- /dev/null +++ b/app/vendor/cakephp/cakephp/.mailmap @@ -0,0 +1,115 @@ +Mark Story +Mark Story +Mark Story +José Lorenzo Rodríguez +José Lorenzo Rodríguez +José Lorenzo Rodríguez José Lorenzo Rodríguez Urdaneta +ADmad +Mathew Foscarini +Mathew Foscarini +Ceeram +Ceeram +Walther Lalk +Walther Lalk +Walther Lalk +Walther Lalk +Walther Lalk +Mark Scherer +Mark Scherer +Mark Scherer +phpnut +phpnut +AD7six +AD7six +predominant +mariano.iglesias +mariano.iglesias +antograssiot +Florian Krämer +Florian Krämer +Florian Krämer +Rachman Chavik +Rachman Chavik +jperras +renan.saddam +Ber Clausen +Marc Würth +Jad Bitar +Jad Bitar +Jad Bitar +Yves P +dogmatic69 +Majna +Robert Pustułka +Robert Pustułka +Robert Pustułka +Thomas Ploch +Tigran Gabrielyan +Bryan Crowe +Bryan Crowe +Sam +Jorge González +Saleh Souzanchi +Yevgeny Tomenko +Ricardo Arturo Cabral +Cauan Cabral +pirouet +davidsteinsland +jamiemill +Stefan Dickmann +Benjamin Tamási +Gordon Pettey (petteyg) +Mathieu de Ruiter +Cees-Jan Kiewiet +Fiblan +Haithem BEN GHORBAL +Pedro Perejón +Algirdas Gurevicius +Calin +Mikaël Capelle +OKINAKA Kenshin +Walter Nasich +mstra001 +Aymeric Derbois +Daniel +James Michael DuPont +James Michael DuPont +Jan Dorsman +Pierre Martin +Jeremy Harris +Jeremy Harris +Christian Winther +Christian Winther +Christian Winther +Jose Diaz-Gonzalez +Jose Diaz-Gonzalez +Jose Diaz-Gonzalez +Frank de Graaf +Frank de Graaf Frank de Graaf +Frank de Graaf +Marlin Cremers +Marlin Cremers +Marlin Cremers +David Yell +James Watts +Jonas Hartmann +Jonas Hartmann +Jonas Hartmann +Thom Seddon +Thom Seddon +Robbert Noordzij +Robbert Noordzij +Simon East +Simon East +Matt Alexander +Matt Alexander +Mark van Driel +Mark van Driel +Juan Basso +Juan Basso +antograssiot +antograssiot +Patrick Conroy +Patrick Conroy +saeideng +saeideng diff --git a/app/vendor/cakephp/cakephp/.varci.yml b/app/vendor/cakephp/cakephp/.varci.yml new file mode 100644 index 000000000..a9fd806f0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/.varci.yml @@ -0,0 +1,44 @@ +ruleset: + label_defects: + name: "Label defects" + events: [ issues, pull_request ] + label: Defect + when: + - action = "opened" + - body matches "/\[\s?x\s?\] bug/" + + label_enhancements: + name: "Label enhancements" + events: [ issues, pull_request ] + label: Enhancement + when: + - action = "opened" + - body matches "/\[\s?x\s?\] enhancement/" + + label_rfcs: + name: "Label RFCs" + events: [ issues ] + label: RFC + when: + - action = "opened" + - body matches "/\[\s?x\s?\] feature\-discussion/" + + remove_invalid: + name: "Remove invalid tag when issue re-opened" + events: [ issues, pull_request ] + label: -Invalid + when: + - action = "reopened" + - filter(labels, "name") has "Invalid" + + request_missing_version: + name: "Request missing version" + events: [ issues ] + label: "On hold" + when: + - action = "opened" or action = "re-opened" + - body matches "/\[x\] bug/" + - 'not(body matches "/CakePHP Version: v?(\d+\.)?(\d+\.)?(\*|\d+)/")' + - 'not(body matches "/CakePHP Version: [0-9a-f]{5,40}/")' + comment: '{{ user.login }}, please include the CakePHP version number you are using in your description. It helps us debug your issue.' + diff --git a/app/vendor/cakephp/cakephp/LICENSE b/app/vendor/cakephp/cakephp/LICENSE new file mode 100644 index 000000000..90862ff0e --- /dev/null +++ b/app/vendor/cakephp/cakephp/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2005-2018, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/app/vendor/cakephp/cakephp/README.md b/app/vendor/cakephp/cakephp/README.md new file mode 100644 index 000000000..f452017be --- /dev/null +++ b/app/vendor/cakephp/cakephp/README.md @@ -0,0 +1,91 @@ +

+ + CakePHP + +

+

+ + Software License + + + Build Status + + + Coverage Status + + + Code Consistency + + + Total Downloads + + + Latest Stable Version + +

+ +[CakePHP](https://cakephp.org) is a rapid development framework for PHP which +uses commonly known design patterns like Associative Data +Mapping, Front Controller, and MVC. Our primary goal is to provide a structured +framework that enables PHP users at all levels to rapidly develop robust web +applications, without any loss to flexibility. + +## Installing CakePHP via Composer + +You can install CakePHP into your project using +[Composer](https://getcomposer.org). If you're starting a new project, we +recommend using the [app skeleton](https://github.com/cakephp/app) as +a starting point. For existing applications you can run the following: + +``` bash +$ composer require cakephp/cakephp:"~3.6" +``` + +## Running Tests + +Assuming you have PHPUnit installed system wide using one of the methods stated +[here](https://phpunit.de/manual/current/en/installation.html), you can run the +tests for CakePHP by doing the following: + +1. Copy `phpunit.xml.dist` to `phpunit.xml`. +2. Add the relevant database credentials to your `phpunit.xml` if you want to run tests against + a non-SQLite datasource. +3. Run `phpunit`. + +## Some Handy Links + +* [CakePHP](https://cakephp.org) - The rapid development PHP framework. +* [CookBook](https://book.cakephp.org) - The CakePHP user documentation; start learning here! +* [API](https://api.cakephp.org) - A reference to CakePHP's classes. +* [Awesome CakePHP](https://github.com/FriendsOfCake/awesome-cakephp) - A list of featured resources around the framework. +* [Plugins](https://plugins.cakephp.org) - A repository of extensions to the framework. +* [The Bakery](https://bakery.cakephp.org) - Tips, tutorials and articles. +* [Community Center](https://community.cakephp.org) - A source for everything community related. +* [Training](https://training.cakephp.org) - Join a live session and get skilled with the framework. +* [CakeFest](https://cakefest.org) - Don't miss our annual CakePHP conference. +* [Cake Software Foundation](https://cakefoundation.org) - Promoting development related to CakePHP. + +## Get Support! + +* [Slack](https://cakesf.herokuapp.com/) - Join us on Slack. +* [#cakephp](https://webchat.freenode.net/?channels=#cakephp) on irc.freenode.net - Come chat with us, we have cake. +* [Forum](https://discourse.cakephp.org/) - Official CakePHP forum. +* [GitHub Issues](https://github.com/cakephp/cakephp/issues) - Got issues? Please tell us! +* [Roadmaps](https://github.com/cakephp/cakephp/wiki#roadmaps) - Want to contribute? Get involved! + +## Contributing + +* [CONTRIBUTING.md](.github/CONTRIBUTING.md) - Quick pointers for contributing to the CakePHP project. +* [CookBook "Contributing" Section](https://book.cakephp.org/3.0/en/contributing.html) - Details about contributing to the project. + +# Security + +If you’ve found a security issue in CakePHP, please use the following procedure instead of the normal bug reporting system. Instead of using the bug tracker, mailing list or IRC please send an email to security [at] cakephp.org. Emails sent to this address go to the CakePHP core team on a private mailing list. + +For each report, we try to first confirm the vulnerability. Once confirmed, the CakePHP team will take the following actions: + +- Acknowledge to the reporter that we’ve received the issue, and are working on a fix. We ask that the reporter keep the issue confidential until we announce it. +- Get a fix/patch prepared. +- Prepare a post describing the vulnerability, and the possible exploits. +- Release new versions of all affected versions. +- Prominently feature the problem in the release announcement. diff --git a/app/vendor/cakephp/cakephp/VERSION.txt b/app/vendor/cakephp/cakephp/VERSION.txt new file mode 100644 index 000000000..110f4d625 --- /dev/null +++ b/app/vendor/cakephp/cakephp/VERSION.txt @@ -0,0 +1,19 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +--------------------------------------------------------------------------------------------+ // +// CakePHP Version +// +// Holds a static string representing the current version of CakePHP +// +// CakePHP(tm) : Rapid Development Framework (https://cakephp.org) +// Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) +// +// Licensed under The MIT License +// Redistributions of files must retain the above copyright notice. +// +// @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) +// @link https://cakephp.org +// @since CakePHP(tm) v 0.2.9 +// @license https://opensource.org/licenses/mit-license.php MIT License +// +--------------------------------------------------------------------------------------------+ // +//////////////////////////////////////////////////////////////////////////////////////////////////// +3.6.7 diff --git a/app/vendor/cakephp/cakephp/composer.json b/app/vendor/cakephp/cakephp/composer.json new file mode 100644 index 000000000..8818ca52c --- /dev/null +++ b/app/vendor/cakephp/cakephp/composer.json @@ -0,0 +1,100 @@ +{ + "name": "cakephp/cakephp", + "description": "The CakePHP framework", + "type": "library", + "keywords": [ + "framework", + "mvc", + "rapid-development", + "conventions over configuration", + "dry", + "orm", + "form", + "validation", + "psr-7" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/cakephp/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/cakephp" + }, + "require": { + "php": ">=5.6.0", + "ext-intl": "*", + "ext-mbstring": "*", + "cakephp/chronos": "^1.0.1", + "aura/intl": "^3.0.0", + "psr/log": "^1.0.0", + "zendframework/zend-diactoros": "^1.4.0" + }, + "suggest": { + "ext-openssl": "To use Security::encrypt() or have secure CSRF token generation.", + "lib-ICU": "The intl PHP library, to use Text::transliterate() or Text::slug()" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.14|^6.0", + "cakephp/cakephp-codesniffer": "^3.0" + }, + "autoload": { + "psr-4": { + "Cake\\": "src/" + }, + "files": [ + "src/Core/functions.php", + "src/Collection/functions.php", + "src/I18n/functions.php", + "src/Utility/bootstrap.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Cake\\PHPStan\\": "tests/PHPStan/", + "Cake\\Test\\": "tests/", + "TestApp\\": "tests/test_app/TestApp/", + "TestPlugin\\": "tests/test_app/Plugin/TestPlugin/src/", + "TestPlugin\\Test\\": "tests/test_app/Plugin/TestPlugin/tests/", + "TestPluginTwo\\": "tests/test_app/Plugin/TestPluginTwo/src/", + "Company\\TestPluginThree\\": "tests/test_app/Plugin/Company/TestPluginThree/src/", + "Company\\TestPluginThree\\Test\\": "tests/test_app/Plugin/Company/TestPluginThree/tests/", + "ParentPlugin\\": "tests/test_app/Plugin/ParentPlugin/src/", + "PluginJs\\": "tests/test_app/Plugin/PluginJs/src/" + } + }, + "replace": { + "cakephp/cache": "self.version", + "cakephp/collection": "self.version", + "cakephp/core": "self.version", + "cakephp/datasource": "self.version", + "cakephp/database": "self.version", + "cakephp/event": "self.version", + "cakephp/filesystem": "self.version", + "cakephp/form": "self.version", + "cakephp/i18n": "self.version", + "cakephp/log": "self.version", + "cakephp/orm": "self.version", + "cakephp/utility": "self.version", + "cakephp/validation": "self.version" + }, + "conflict": { + "phpunit/phpunit": "<5.7" + }, + "scripts": { + "check": [ + "@cs-check", + "@test" + ], + "cs-check": "phpcs --colors -p ./src ./tests", + "cs-fix": "phpcbf --colors ./src ./tests", + "test": "phpunit", + "test-coverage": "phpunit --coverage-clover=clover.xml" + } +} diff --git a/app/vendor/cakephp/cakephp/config/bootstrap.php b/app/vendor/cakephp/cakephp/config/bootstrap.php new file mode 100644 index 000000000..1e40cbfde --- /dev/null +++ b/app/vendor/cakephp/cakephp/config/bootstrap.php @@ -0,0 +1,23 @@ + trim(array_pop($versionFile)) +]; diff --git a/app/vendor/cakephp/cakephp/contrib/pre-commit b/app/vendor/cakephp/cakephp/contrib/pre-commit new file mode 100644 index 000000000..17332fc62 --- /dev/null +++ b/app/vendor/cakephp/cakephp/contrib/pre-commit @@ -0,0 +1,38 @@ +#!/bin/sh +FILES=`git diff --cached --name-only --diff-filter=ACMR HEAD | grep \\\\.php` +PROJECT=`php -r "echo dirname(dirname(realpath('$0')));"` + +# Determine if a file list is passed +if [ "$#" -eq 1 ] +then + oIFS=$IFS + IFS=' + ' + SFILES="$1" + IFS=$oIFS +fi +SFILES=${SFILES:-$FILES} + +echo "Checking PHP Lint..." +for FILE in $SFILES +do + php -l -d display_errors=0 $PROJECT/$FILE + if [ $? != 0 ] + then + echo "Fix the error before commit." + exit 1 + fi + FILES="$FILES $PROJECT/$FILE" +done + +if [ "$SFILES" != "" ] +then + echo "Running PHPCS" + ./vendor/bin/phpcs --standard=vendor/cakephp/cakephp-codesniffer/CakePHP $SFILES + if [ $? != 0 ] + then + echo "PHPCS Errors found; commit aborted." + exit 1 + fi +fi +exit $? diff --git a/app/vendor/cakephp/cakephp/phpcs.xml.dist b/app/vendor/cakephp/cakephp/phpcs.xml.dist new file mode 100644 index 000000000..9d4c21b75 --- /dev/null +++ b/app/vendor/cakephp/cakephp/phpcs.xml.dist @@ -0,0 +1,9 @@ + + + + + + + 0 + + diff --git a/app/vendor/cakephp/cakephp/phpstan.neon b/app/vendor/cakephp/cakephp/phpstan.neon new file mode 100644 index 000000000..bc2248cef --- /dev/null +++ b/app/vendor/cakephp/cakephp/phpstan.neon @@ -0,0 +1,39 @@ +parameters: + autoload_files: + - tests/bootstrap.php + ignoreErrors: + - '#Function wincache_ucache_[a-zA-Z0-9_]+ not found#' + - '#Function xcache_[a-zA-Z0-9_]+ not found#' + - '#Cake\\Database\\Type\\[a-zA-Z0-9_]+Type::__construct\(\) does not call parent constructor from Cake\\Database\\Type#' + - '#Constructor of class Cake\\[a-zA-Z0-9_\\]+ has an unused parameter#' + - '#Access to undefined constant Memcached::OPT_CLIENT_MODE#' + - '#Access to undefined constant Memcached::DYNAMIC_CLIENT_MODE#' + - '#Access to undefined constant PDO::SQLSRV_ATTR_ENCODING#' + - '#Access to undefined constant PDO::SQLSRV_ENCODING_BINARY#' + - '#Constant XC_TYPE_VAR not found#' + - '#Call to an undefined method Psr\\Http\\Message\\ResponseInterface::getCookies\(\)#' + - '#Access to an undefined property Psr\\Http\\Message\\UriInterface::\$webroot#' + - '#Access to an undefined property Psr\\Http\\Message\\UriInterface::\$base#' + - '#Result of method Cake\\Http\\Response::send\(\) \(void\) is used#' + - '#Method Cake\\View\\Form\\ContextInterface::val\(\) invoked with 2 parameters, 1 required#' + - '#Access to an undefined property Exception::\$queryString#' + - '#Access to an undefined property PHPUnit\\Framework\\Test::\$fixtureManager#' + - '#Method Redis::#' + - '#Call to an undefined method Traversable::getArrayCopy().#' + - '#Variable \$config in isset\(\) is never defined#' + - '#Call to static method id\(\) on an unknown class PHPUnit_Runner_Version#' + - '#Call to an undefined method DateTimeInterface::i18nFormat\(\)#' + - '#Call to an undefined method object::__toString\(\)#' + - '#Call to an undefined method object::toArray\(\)#' + - '#Call to an undefined method object::__debugInfo\(\)#' + - '#Cannot call method lastInsertId\(\) on null#' + earlyTerminatingMethodCalls: + Cake\Shell\Shell: + - abort + +services: + - + class: Cake\PHPStan\AssociationTableMixinClassReflectionExtension + tags: + - phpstan.broker.methodsClassReflectionExtension + - phpstan.broker.propertiesClassReflectionExtension diff --git a/app/vendor/cakephp/cakephp/src/Auth/AbstractPasswordHasher.php b/app/vendor/cakephp/cakephp/src/Auth/AbstractPasswordHasher.php new file mode 100644 index 000000000..3aef37caf --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/AbstractPasswordHasher.php @@ -0,0 +1,79 @@ +setConfig($config); + } + + /** + * Generates password hash. + * + * @param string|array $password Plain text password to hash or array of data + * required to generate password hash. + * @return string Password hash + */ + abstract public function hash($password); + + /** + * Check hash. Generate hash from user provided password string or data array + * and check against existing hash. + * + * @param string|array $password Plain text password to hash or data array. + * @param string $hashedPassword Existing hashed password. + * @return bool True if hashes match else false. + */ + abstract public function check($password, $hashedPassword); + + /** + * Returns true if the password need to be rehashed, due to the password being + * created with anything else than the passwords generated by this class. + * + * Returns true by default since the only implementation users should rely + * on is the one provided by default in php 5.5+ or any compatible library + * + * @param string $password The password to verify + * @return bool + */ + public function needsRehash($password) + { + return password_needs_rehash($password, PASSWORD_DEFAULT); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/BaseAuthenticate.php b/app/vendor/cakephp/cakephp/src/Auth/BaseAuthenticate.php new file mode 100644 index 000000000..d5678d2d1 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/BaseAuthenticate.php @@ -0,0 +1,268 @@ + ['some_finder_option' => 'some_value']] + * - `passwordHasher` Password hasher class. Can be a string specifying class name + * or an array containing `className` key, any other keys will be passed as + * config to the class. Defaults to 'Default'. + * - Options `scope` and `contain` have been deprecated since 3.1. Use custom + * finder instead to modify the query to fetch user record. + * + * @var array + */ + protected $_defaultConfig = [ + 'fields' => [ + 'username' => 'username', + 'password' => 'password' + ], + 'userModel' => 'Users', + 'scope' => [], + 'finder' => 'all', + 'contain' => null, + 'passwordHasher' => 'Default' + ]; + + /** + * A Component registry, used to get more components. + * + * @var \Cake\Controller\ComponentRegistry + */ + protected $_registry; + + /** + * Password hasher instance. + * + * @var \Cake\Auth\AbstractPasswordHasher + */ + protected $_passwordHasher; + + /** + * Whether or not the user authenticated by this class + * requires their password to be rehashed with another algorithm. + * + * @var bool + */ + protected $_needsPasswordRehash = false; + + /** + * Constructor + * + * @param \Cake\Controller\ComponentRegistry $registry The Component registry used on this request. + * @param array $config Array of config to use. + */ + public function __construct(ComponentRegistry $registry, array $config = []) + { + $this->_registry = $registry; + $this->setConfig($config); + + if ($this->getConfig('scope') || $this->getConfig('contain')) { + deprecationWarning( + 'The `scope` and `contain` options for Authentication are deprecated. ' . + 'Use the `finder` option instead to define additional conditions.' + ); + } + } + + /** + * Find a user record using the username and password provided. + * + * Input passwords will be hashed even when a user doesn't exist. This + * helps mitigate timing attacks that are attempting to find valid usernames. + * + * @param string $username The username/identifier. + * @param string|null $password The password, if not provided password checking is skipped + * and result of find is returned. + * @return bool|array Either false on failure, or an array of user data. + */ + protected function _findUser($username, $password = null) + { + $result = $this->_query($username)->first(); + + if (empty($result)) { + $hasher = $this->passwordHasher(); + $hasher->hash((string)$password); + + return false; + } + + $passwordField = $this->_config['fields']['password']; + if ($password !== null) { + $hasher = $this->passwordHasher(); + $hashedPassword = $result->get($passwordField); + if (!$hasher->check($password, $hashedPassword)) { + return false; + } + + $this->_needsPasswordRehash = $hasher->needsRehash($hashedPassword); + $result->unsetProperty($passwordField); + } + $hidden = $result->getHidden(); + if ($password === null && in_array($passwordField, $hidden)) { + $key = array_search($passwordField, $hidden); + unset($hidden[$key]); + $result->setHidden($hidden); + } + + return $result->toArray(); + } + + /** + * Get query object for fetching user from database. + * + * @param string $username The username/identifier. + * @return \Cake\ORM\Query + */ + protected function _query($username) + { + $config = $this->_config; + $table = $this->getTableLocator()->get($config['userModel']); + + $options = [ + 'conditions' => [$table->aliasField($config['fields']['username']) => $username] + ]; + + if (!empty($config['scope'])) { + $options['conditions'] = array_merge($options['conditions'], $config['scope']); + } + if (!empty($config['contain'])) { + $options['contain'] = $config['contain']; + } + + $finder = $config['finder']; + if (is_array($finder)) { + $options += current($finder); + $finder = key($finder); + } + + if (!isset($options['username'])) { + $options['username'] = $username; + } + + return $table->find($finder, $options); + } + + /** + * Return password hasher object + * + * @return \Cake\Auth\AbstractPasswordHasher Password hasher instance + * @throws \RuntimeException If password hasher class not found or + * it does not extend AbstractPasswordHasher + */ + public function passwordHasher() + { + if ($this->_passwordHasher) { + return $this->_passwordHasher; + } + + $passwordHasher = $this->_config['passwordHasher']; + + return $this->_passwordHasher = PasswordHasherFactory::build($passwordHasher); + } + + /** + * Returns whether or not the password stored in the repository for the logged in user + * requires to be rehashed with another algorithm + * + * @return bool + */ + public function needsPasswordRehash() + { + return $this->_needsPasswordRehash; + } + + /** + * Authenticate a user based on the request information. + * + * @param \Cake\Http\ServerRequest $request Request to get authentication information from. + * @param \Cake\Http\Response $response A response object that can have headers added. + * @return mixed Either false on failure, or an array of user data on success. + */ + abstract public function authenticate(ServerRequest $request, Response $response); + + /** + * Get a user based on information in the request. Primarily used by stateless authentication + * systems like basic and digest auth. + * + * @param \Cake\Http\ServerRequest $request Request object. + * @return mixed Either false or an array of user information + */ + public function getUser(ServerRequest $request) + { + return false; + } + + /** + * Handle unauthenticated access attempt. In implementation valid return values + * can be: + * + * - Null - No action taken, AuthComponent should return appropriate response. + * - Cake\Http\Response - A response object, which will cause AuthComponent to + * simply return that response. + * + * @param \Cake\Http\ServerRequest $request A request object. + * @param \Cake\Http\Response $response A response object. + * @return void + */ + public function unauthenticated(ServerRequest $request, Response $response) + { + } + + /** + * Returns a list of all events that this authenticate class will listen to. + * + * An authenticate class can listen to following events fired by AuthComponent: + * + * - `Auth.afterIdentify` - Fired after a user has been identified using one of + * configured authenticate class. The callback function should have signature + * like `afterIdentify(Event $event, array $user)` when `$user` is the + * identified user record. + * + * - `Auth.logout` - Fired when AuthComponent::logout() is called. The callback + * function should have signature like `logout(Event $event, array $user)` + * where `$user` is the user about to be logged out. + * + * @return array List of events this class listens to. Defaults to `[]`. + */ + public function implementedEvents() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/BaseAuthorize.php b/app/vendor/cakephp/cakephp/src/Auth/BaseAuthorize.php new file mode 100644 index 000000000..d03092479 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/BaseAuthorize.php @@ -0,0 +1,66 @@ +_registry = $registry; + $this->setConfig($config); + } + + /** + * Checks user authorization. + * + * @param array|\ArrayAccess $user Active user data + * @param \Cake\Http\ServerRequest $request Request instance. + * @return bool + */ + abstract public function authorize($user, ServerRequest $request); +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/BasicAuthenticate.php b/app/vendor/cakephp/cakephp/src/Auth/BasicAuthenticate.php new file mode 100644 index 000000000..9ca589ade --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/BasicAuthenticate.php @@ -0,0 +1,115 @@ + [ + * 'authenticate' => ['Basic'] + * ] + * ]; + * ``` + * + * You should also set `AuthComponent::$sessionKey = false;` in your AppController's + * beforeFilter() to prevent CakePHP from sending a session cookie to the client. + * + * Since HTTP Basic Authentication is stateless you don't need a login() action + * in your controller. The user credentials will be checked on each request. If + * valid credentials are not provided, required authentication headers will be sent + * by this authentication provider which triggers the login dialog in the browser/client. + * + * You may also want to use `$this->Auth->unauthorizedRedirect = false;`. + * By default, unauthorized users are redirected to the referrer URL, + * `AuthComponent::$loginAction`, or '/'. If unauthorizedRedirect is set to + * false, a ForbiddenException exception is thrown instead of redirecting. + */ +class BasicAuthenticate extends BaseAuthenticate +{ + + /** + * Authenticate a user using HTTP auth. Will use the configured User model and attempt a + * login using HTTP auth. + * + * @param \Cake\Http\ServerRequest $request The request to authenticate with. + * @param \Cake\Http\Response $response The response to add headers to. + * @return mixed Either false on failure, or an array of user data on success. + */ + public function authenticate(ServerRequest $request, Response $response) + { + return $this->getUser($request); + } + + /** + * Get a user based on information in the request. Used by cookie-less auth for stateless clients. + * + * @param \Cake\Http\ServerRequest $request Request object. + * @return mixed Either false or an array of user information + */ + public function getUser(ServerRequest $request) + { + $username = $request->getEnv('PHP_AUTH_USER'); + $pass = $request->getEnv('PHP_AUTH_PW'); + + if (!is_string($username) || $username === '' || !is_string($pass) || $pass === '') { + return false; + } + + return $this->_findUser($username, $pass); + } + + /** + * Handles an unauthenticated access attempt by sending appropriate login headers + * + * @param \Cake\Http\ServerRequest $request A request object. + * @param \Cake\Http\Response $response A response object. + * @return void + * @throws \Cake\Http\Exception\UnauthorizedException + */ + public function unauthenticated(ServerRequest $request, Response $response) + { + $Exception = new UnauthorizedException(); + $Exception->responseHeader($this->loginHeaders($request)); + throw $Exception; + } + + /** + * Generate the login headers + * + * @param \Cake\Http\ServerRequest $request Request object. + * @return array Headers for logging in. + */ + public function loginHeaders(ServerRequest $request) + { + $realm = $this->getConfig('realm') ?: $request->getEnv('SERVER_NAME'); + + return [ + 'WWW-Authenticate' => sprintf('Basic realm="%s"', $realm) + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/ControllerAuthorize.php b/app/vendor/cakephp/cakephp/src/Auth/ControllerAuthorize.php new file mode 100644 index 000000000..fc69fab68 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/ControllerAuthorize.php @@ -0,0 +1,95 @@ +request->getParam('admin')) { + * return $user['role'] === 'admin'; + * } + * return !empty($user); + * } + * ``` + * + * The above is simple implementation that would only authorize users of the + * 'admin' role to access admin routing. + * + * @see \Cake\Controller\Component\AuthComponent::$authenticate + */ +class ControllerAuthorize extends BaseAuthorize +{ + + /** + * Controller for the request. + * + * @var \Cake\Controller\Controller + */ + protected $_Controller; + + /** + * {@inheritDoc} + */ + public function __construct(ComponentRegistry $registry, array $config = []) + { + parent::__construct($registry, $config); + $this->controller($registry->getController()); + } + + /** + * Get/set the controller this authorize object will be working with. Also + * checks that isAuthorized is implemented. + * + * @param \Cake\Controller\Controller|null $controller null to get, a controller to set. + * @return \Cake\Controller\Controller + * @throws \Cake\Core\Exception\Exception If controller does not have method `isAuthorized()`. + */ + public function controller(Controller $controller = null) + { + if ($controller) { + if (!method_exists($controller, 'isAuthorized')) { + throw new Exception(sprintf( + '%s does not implement an isAuthorized() method.', + get_class($controller) + )); + } + $this->_Controller = $controller; + } + + return $this->_Controller; + } + + /** + * Checks user authorization using a controller callback. + * + * @param array|\ArrayAccess $user Active user data + * @param \Cake\Http\ServerRequest $request Request instance. + * @return bool + */ + public function authorize($user, ServerRequest $request) + { + return (bool)$this->_Controller->isAuthorized($user); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/DefaultPasswordHasher.php b/app/vendor/cakephp/cakephp/src/Auth/DefaultPasswordHasher.php new file mode 100644 index 000000000..dc9420cdd --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/DefaultPasswordHasher.php @@ -0,0 +1,79 @@ + PASSWORD_DEFAULT, + 'hashOptions' => [] + ]; + + /** + * Generates password hash. + * + * @param string $password Plain text password to hash. + * @return bool|string Password hash or false on failure + * @link https://book.cakephp.org/3.0/en/controllers/components/authentication.html#hashing-passwords + */ + public function hash($password) + { + return password_hash( + $password, + $this->_config['hashType'], + $this->_config['hashOptions'] + ); + } + + /** + * Check hash. Generate hash for user provided password and check against existing hash. + * + * @param string $password Plain text password to hash. + * @param string $hashedPassword Existing hashed password. + * @return bool True if hashes match else false. + */ + public function check($password, $hashedPassword) + { + return password_verify($password, $hashedPassword); + } + + /** + * Returns true if the password need to be rehashed, due to the password being + * created with anything else than the passwords generated by this class. + * + * @param string $password The password to verify + * @return bool + */ + public function needsRehash($password) + { + return password_needs_rehash($password, $this->_config['hashType'], $this->_config['hashOptions']); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/DigestAuthenticate.php b/app/vendor/cakephp/cakephp/src/Auth/DigestAuthenticate.php new file mode 100644 index 000000000..61435294c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/DigestAuthenticate.php @@ -0,0 +1,287 @@ + [ + * 'authenticate' => ['Digest'] + * ] + * ]; + * ``` + * + * You should also set `AuthComponent::$sessionKey = false;` in your AppController's + * beforeFilter() to prevent CakePHP from sending a session cookie to the client. + * + * Since HTTP Digest Authentication is stateless you don't need a login() action + * in your controller. The user credentials will be checked on each request. If + * valid credentials are not provided, required authentication headers will be sent + * by this authentication provider which triggers the login dialog in the browser/client. + * + * You may also want to use `$this->Auth->unauthorizedRedirect = false;`. + * This causes AuthComponent to throw a ForbiddenException exception instead of + * redirecting to another page. + * + * ### Generating passwords compatible with Digest authentication. + * + * DigestAuthenticate requires a special password hash that conforms to RFC2617. + * You can generate this password using `DigestAuthenticate::password()` + * + * ``` + * $digestPass = DigestAuthenticate::password($username, $password, env('SERVER_NAME')); + * ``` + * + * If you wish to use digest authentication alongside other authentication methods, + * it's recommended that you store the digest authentication separately. For + * example `User.digest_pass` could be used for a digest password, while + * `User.password` would store the password hash for use with other methods like + * Basic or Form. + */ +class DigestAuthenticate extends BasicAuthenticate +{ + + /** + * Constructor + * + * Besides the keys specified in BaseAuthenticate::$_defaultConfig, + * DigestAuthenticate uses the following extra keys: + * + * - `secret` The secret to use for nonce validation. Defaults to Security::getSalt(). + * - `realm` The realm authentication is for, Defaults to the servername. + * - `qop` Defaults to 'auth', no other values are supported at this time. + * - `opaque` A string that must be returned unchanged by clients. + * Defaults to `md5($config['realm'])` + * - `nonceLifetime` The number of seconds that nonces are valid for. Defaults to 300. + * + * @param \Cake\Controller\ComponentRegistry $registry The Component registry + * used on this request. + * @param array $config Array of config to use. + */ + public function __construct(ComponentRegistry $registry, array $config = []) + { + $this->setConfig([ + 'nonceLifetime' => 300, + 'secret' => Security::getSalt(), + 'realm' => null, + 'qop' => 'auth', + 'opaque' => null, + ]); + + parent::__construct($registry, $config); + } + + /** + * Get a user based on information in the request. Used by cookie-less auth for stateless clients. + * + * @param \Cake\Http\ServerRequest $request Request object. + * @return mixed Either false or an array of user information + */ + public function getUser(ServerRequest $request) + { + $digest = $this->_getDigest($request); + if (empty($digest)) { + return false; + } + + $user = $this->_findUser($digest['username']); + if (empty($user)) { + return false; + } + + if (!$this->validNonce($digest['nonce'])) { + return false; + } + + $field = $this->_config['fields']['password']; + $password = $user[$field]; + unset($user[$field]); + + $hash = $this->generateResponseHash($digest, $password, $request->getEnv('ORIGINAL_REQUEST_METHOD')); + if (hash_equals($hash, $digest['response'])) { + return $user; + } + + return false; + } + + /** + * Gets the digest headers from the request/environment. + * + * @param \Cake\Http\ServerRequest $request Request object. + * @return array|bool Array of digest information. + */ + protected function _getDigest(ServerRequest $request) + { + $digest = $request->getEnv('PHP_AUTH_DIGEST'); + if (empty($digest) && function_exists('apache_request_headers')) { + $headers = apache_request_headers(); + if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') { + $digest = substr($headers['Authorization'], 7); + } + } + if (empty($digest)) { + return false; + } + + return $this->parseAuthData($digest); + } + + /** + * Parse the digest authentication headers and split them up. + * + * @param string $digest The raw digest authentication headers. + * @return array|null An array of digest authentication headers + */ + public function parseAuthData($digest) + { + if (substr($digest, 0, 7) === 'Digest ') { + $digest = substr($digest, 7); + } + $keys = $match = []; + $req = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; + preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER); + + foreach ($match as $i) { + $keys[$i[1]] = $i[3]; + unset($req[$i[1]]); + } + + if (empty($req)) { + return $keys; + } + + return null; + } + + /** + * Generate the response hash for a given digest array. + * + * @param array $digest Digest information containing data from DigestAuthenticate::parseAuthData(). + * @param string $password The digest hash password generated with DigestAuthenticate::password() + * @param string $method Request method + * @return string Response hash + */ + public function generateResponseHash($digest, $password, $method) + { + return md5( + $password . + ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . + md5($method . ':' . $digest['uri']) + ); + } + + /** + * Creates an auth digest password hash to store + * + * @param string $username The username to use in the digest hash. + * @param string $password The unhashed password to make a digest hash for. + * @param string $realm The realm the password is for. + * @return string the hashed password that can later be used with Digest authentication. + */ + public static function password($username, $password, $realm) + { + return md5($username . ':' . $realm . ':' . $password); + } + + /** + * Generate the login headers + * + * @param \Cake\Http\ServerRequest $request Request object. + * @return array Headers for logging in. + */ + public function loginHeaders(ServerRequest $request) + { + $realm = $this->_config['realm'] ?: $request->getEnv('SERVER_NAME'); + + $options = [ + 'realm' => $realm, + 'qop' => $this->_config['qop'], + 'nonce' => $this->generateNonce(), + 'opaque' => $this->_config['opaque'] ?: md5($realm) + ]; + + $digest = $this->_getDigest($request); + if ($digest && isset($digest['nonce']) && !$this->validNonce($digest['nonce'])) { + $options['stale'] = true; + } + + $opts = []; + foreach ($options as $k => $v) { + if (is_bool($v)) { + $v = $v ? 'true' : 'false'; + $opts[] = sprintf('%s=%s', $k, $v); + } else { + $opts[] = sprintf('%s="%s"', $k, $v); + } + } + + return [ + 'WWW-Authenticate' => 'Digest ' . implode(',', $opts) + ]; + } + + /** + * Generate a nonce value that is validated in future requests. + * + * @return string + */ + protected function generateNonce() + { + $expiryTime = microtime(true) + $this->getConfig('nonceLifetime'); + $secret = $this->getConfig('secret'); + $signatureValue = hash_hmac('sha256', $expiryTime . ':' . $secret, $secret); + $nonceValue = $expiryTime . ':' . $signatureValue; + + return base64_encode($nonceValue); + } + + /** + * Check the nonce to ensure it is valid and not expired. + * + * @param string $nonce The nonce value to check. + * @return bool + */ + protected function validNonce($nonce) + { + $value = base64_decode($nonce); + if ($value === false) { + return false; + } + $parts = explode(':', $value); + if (count($parts) !== 2) { + return false; + } + list($expires, $checksum) = $parts; + if ($expires < microtime(true)) { + return false; + } + $secret = $this->getConfig('secret'); + $check = hash_hmac('sha256', $expires . ':' . $secret, $secret); + + return hash_equals($check, $checksum); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/FallbackPasswordHasher.php b/app/vendor/cakephp/cakephp/src/Auth/FallbackPasswordHasher.php new file mode 100644 index 000000000..7f7e7cd1b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/FallbackPasswordHasher.php @@ -0,0 +1,104 @@ + [] + ]; + + /** + * Holds the list of password hasher objects that will be used + * + * @var array + */ + protected $_hashers = []; + + /** + * Constructor + * + * @param array $config configuration options for this object. Requires the + * `hashers` key to be present in the array with a list of other hashers to be + * used + */ + public function __construct(array $config = []) + { + parent::__construct($config); + foreach ($this->_config['hashers'] as $key => $hasher) { + if (is_array($hasher) && !isset($hasher['className'])) { + $hasher['className'] = $key; + } + $this->_hashers[] = PasswordHasherFactory::build($hasher); + } + } + + /** + * Generates password hash. + * + * Uses the first password hasher in the list to generate the hash + * + * @param string $password Plain text password to hash. + * @return string Password hash + */ + public function hash($password) + { + return $this->_hashers[0]->hash($password); + } + + /** + * Verifies that the provided password corresponds to its hashed version + * + * This will iterate over all configured hashers until one of them returns + * true. + * + * @param string $password Plain text password to hash. + * @param string $hashedPassword Existing hashed password. + * @return bool True if hashes match else false. + */ + public function check($password, $hashedPassword) + { + foreach ($this->_hashers as $hasher) { + if ($hasher->check($password, $hashedPassword)) { + return true; + } + } + + return false; + } + + /** + * Returns true if the password need to be rehashed, with the first hasher present + * in the list of hashers + * + * @param string $password The password to verify + * @return bool + */ + public function needsRehash($password) + { + return $this->_hashers[0]->needsRehash($password); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/FormAuthenticate.php b/app/vendor/cakephp/cakephp/src/Auth/FormAuthenticate.php new file mode 100644 index 000000000..d2311c9a0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/FormAuthenticate.php @@ -0,0 +1,81 @@ +Auth->authenticate = [ + * 'Form' => [ + * 'finder' => ['auth' => ['some_finder_option' => 'some_value']] + * ] + * ] + * ``` + * + * When configuring FormAuthenticate you can pass in config to which fields, model and additional conditions + * are used. See FormAuthenticate::$_config for more information. + * + * @see \Cake\Controller\Component\AuthComponent::$authenticate + */ +class FormAuthenticate extends BaseAuthenticate +{ + + /** + * Checks the fields to ensure they are supplied. + * + * @param \Cake\Http\ServerRequest $request The request that contains login information. + * @param array $fields The fields to be checked. + * @return bool False if the fields have not been supplied. True if they exist. + */ + protected function _checkFields(ServerRequest $request, array $fields) + { + foreach ([$fields['username'], $fields['password']] as $field) { + $value = $request->getData($field); + if (empty($value) || !is_string($value)) { + return false; + } + } + + return true; + } + + /** + * Authenticates the identity contained in a request. Will use the `config.userModel`, and `config.fields` + * to find POST data that is used to find a matching record in the `config.userModel`. Will return false if + * there is no post data, either username or password is missing, or if the scope conditions have not been met. + * + * @param \Cake\Http\ServerRequest $request The request that contains login information. + * @param \Cake\Http\Response $response Unused response object. + * @return mixed False on login failure. An array of User data on success. + */ + public function authenticate(ServerRequest $request, Response $response) + { + $fields = $this->_config['fields']; + if (!$this->_checkFields($request, $fields)) { + return false; + } + + return $this->_findUser( + $request->getData($fields['username']), + $request->getData($fields['password']) + ); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/PasswordHasherFactory.php b/app/vendor/cakephp/cakephp/src/Auth/PasswordHasherFactory.php new file mode 100644 index 000000000..aa163d725 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/PasswordHasherFactory.php @@ -0,0 +1,58 @@ +_user; + } + + /** + * {@inheritDoc} + */ + public function write($user) + { + $this->_user = $user; + } + + /** + * {@inheritDoc} + */ + public function delete() + { + $this->_user = null; + } + + /** + * {@inheritDoc} + */ + public function redirectUrl($url = null) + { + if ($url === null) { + return $this->_redirectUrl; + } + + if ($url === false) { + $this->_redirectUrl = null; + + return null; + } + + $this->_redirectUrl = $url; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/Storage/SessionStorage.php b/app/vendor/cakephp/cakephp/src/Auth/Storage/SessionStorage.php new file mode 100644 index 000000000..749e88ebb --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/Storage/SessionStorage.php @@ -0,0 +1,140 @@ + 'Auth.User', + 'redirect' => 'Auth.redirect' + ]; + + /** + * Constructor. + * + * @param \Cake\Http\ServerRequest $request Request instance. + * @param \Cake\Http\Response $response Response instance. + * @param array $config Configuration list. + */ + public function __construct(ServerRequest $request, Response $response, array $config = []) + { + $this->_session = $request->getSession(); + $this->setConfig($config); + } + + /** + * Read user record from session. + * + * @return array|null User record if available else null. + */ + public function read() + { + if ($this->_user !== null) { + return $this->_user ?: null; + } + + $this->_user = $this->_session->read($this->_config['key']) ?: false; + + return $this->_user ?: null; + } + + /** + * Write user record to session. + * + * The session id is also renewed to help mitigate issues with session replays. + * + * @param array|\ArrayAccess $user User record. + * @return void + */ + public function write($user) + { + $this->_user = $user; + + $this->_session->renew(); + $this->_session->write($this->_config['key'], $user); + } + + /** + * Delete user record from session. + * + * The session id is also renewed to help mitigate issues with session replays. + * + * @return void + */ + public function delete() + { + $this->_user = false; + + $this->_session->delete($this->_config['key']); + $this->_session->renew(); + } + + /** + * {@inheritDoc} + */ + public function redirectUrl($url = null) + { + if ($url === null) { + return $this->_session->read($this->_config['redirect']); + } + + if ($url === false) { + $this->_session->delete($this->_config['redirect']); + + return null; + } + + $this->_session->write($this->_config['redirect'], $url); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Auth/Storage/StorageInterface.php b/app/vendor/cakephp/cakephp/src/Auth/Storage/StorageInterface.php new file mode 100644 index 000000000..5ed9d512c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Auth/Storage/StorageInterface.php @@ -0,0 +1,53 @@ + null + ]; + + /** + * {@inheritDoc} + */ + public function __construct(array $config = []) + { + if (Configure::read('debug')) { + Debugger::checkSecurityKeys(); + } + + parent::__construct($config); + } + + /** + * Generates password hash. + * + * @param string $password Plain text password to hash. + * @return string Password hash + */ + public function hash($password) + { + return Security::hash($password, $this->_config['hashType'], true); + } + + /** + * Check hash. Generate hash for user provided password and check against existing hash. + * + * @param string $password Plain text password to hash. + * @param string $hashedPassword Existing hashed password. + * @return bool True if hashes match else false. + */ + public function check($password, $hashedPassword) + { + return $hashedPassword === $this->hash($password); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Cache/Cache.php b/app/vendor/cakephp/cakephp/src/Cache/Cache.php new file mode 100644 index 000000000..7b74c5f0c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Cache/Cache.php @@ -0,0 +1,670 @@ + 'Cake\Cache\Engine\ApcuEngine', + * 'prefix' => 'my_app_' + * ]); + * ``` + * + * This would configure an APCu cache engine to the 'shared' alias. You could then read and write + * to that cache alias by using it for the `$config` parameter in the various Cache methods. + * + * In general all Cache operations are supported by all cache engines. + * However, Cache::increment() and Cache::decrement() are not supported by File caching. + * + * There are 6 built-in caching engines: + * + * - `FileEngine` - Uses simple files to store content. Poor performance, but good for + * storing large objects, or things that are not IO sensitive. Well suited to development + * as it is an easy cache to inspect and manually flush. + * - `ApcuEngine` - Uses the APCu object cache, one of the fastest caching engines. + * - `MemcacheEngine` - Uses the PECL::Memcache extension and Memcached for storage. + * Fast reads/writes, and benefits from memcache being distributed. + * - `XcacheEngine` - Uses the Xcache extension, an alternative to APCu. + * - `WincacheEngine` - Uses Windows Cache Extension for PHP. Supports wincache 1.1.0 and higher. + * This engine is recommended to people deploying on windows with IIS. + * - `RedisEngine` - Uses redis and php-redis extension to store cache data. + * + * See Cache engine documentation for expected configuration keys. + * + * @see config/app.php for configuration settings + */ +class Cache +{ + + use StaticConfigTrait; + + /** + * An array mapping url schemes to fully qualified caching engine + * class names. + * + * @var array + */ + protected static $_dsnClassMap = [ + 'apc' => 'Cake\Cache\Engine\ApcuEngine', // @deprecated Since 3.6. Use apcu instead. + 'apcu' => 'Cake\Cache\Engine\ApcuEngine', + 'file' => 'Cake\Cache\Engine\FileEngine', + 'memcached' => 'Cake\Cache\Engine\MemcachedEngine', + 'null' => 'Cake\Cache\Engine\NullEngine', + 'redis' => 'Cake\Cache\Engine\RedisEngine', + 'wincache' => 'Cake\Cache\Engine\WincacheEngine', + 'xcache' => 'Cake\Cache\Engine\XcacheEngine', + ]; + + /** + * Flag for tracking whether or not caching is enabled. + * + * @var bool + */ + protected static $_enabled = true; + + /** + * Group to Config mapping + * + * @var array + */ + protected static $_groups = []; + + /** + * Cache Registry used for creating and using cache adapters. + * + * @var \Cake\Core\ObjectRegistry + */ + protected static $_registry; + + /** + * Returns the Cache Registry instance used for creating and using cache adapters. + * + * @return \Cake\Core\ObjectRegistry + */ + public static function getRegistry() + { + if (!static::$_registry) { + static::$_registry = new CacheRegistry(); + } + + return static::$_registry; + } + + /** + * Sets the Cache Registry instance used for creating and using cache adapters. + * + * Also allows for injecting of a new registry instance. + * + * @param \Cake\Core\ObjectRegistry $registry Injectable registry object. + * @return void + */ + public static function setRegistry(ObjectRegistry $registry) + { + static::$_registry = $registry; + } + + /** + * Returns the Cache Registry instance used for creating and using cache adapters. + * Also allows for injecting of a new registry instance. + * + * @param \Cake\Core\ObjectRegistry|null $registry Injectable registry object. + * @return \Cake\Core\ObjectRegistry + * @deprecated Deprecated since 3.5. Use getRegistry() and setRegistry() instead. + */ + public static function registry(ObjectRegistry $registry = null) + { + deprecationWarning('Use Cache::getRegistry() and Cache::setRegistry() instead.'); + if ($registry) { + static::setRegistry($registry); + } + + return static::getRegistry(); + } + + /** + * Finds and builds the instance of the required engine class. + * + * @param string $name Name of the config array that needs an engine instance built + * @return void + * @throws \InvalidArgumentException When a cache engine cannot be created. + */ + protected static function _buildEngine($name) + { + $registry = static::getRegistry(); + + if (empty(static::$_config[$name]['className'])) { + throw new InvalidArgumentException( + sprintf('The "%s" cache configuration does not exist.', $name) + ); + } + + $config = static::$_config[$name]; + + try { + $registry->load($name, $config); + } catch (RuntimeException $e) { + if (!array_key_exists('fallback', $config)) { + $registry->set($name, new NullEngine()); + trigger_error($e->getMessage(), E_USER_WARNING); + + return; + } + + if ($config['fallback'] === false) { + throw $e; + } + + if ($config['fallback'] === $name) { + throw new InvalidArgumentException(sprintf('"%s" cache configuration cannot fallback to itself.', $name), null, $e); + } + + $fallbackEngine = clone static::engine($config['fallback']); + $newConfig = $config + ['groups' => [], 'prefix' => null]; + $fallbackEngine->setConfig('groups', $newConfig['groups'], false); + if ($newConfig['prefix']) { + $fallbackEngine->setConfig('prefix', $newConfig['prefix'], false); + } + $registry->set($name, $fallbackEngine); + } + + if ($config['className'] instanceof CacheEngine) { + $config = $config['className']->getConfig(); + } + + if (!empty($config['groups'])) { + foreach ($config['groups'] as $group) { + static::$_groups[$group][] = $name; + static::$_groups[$group] = array_unique(static::$_groups[$group]); + sort(static::$_groups[$group]); + } + } + } + + /** + * Fetch the engine attached to a specific configuration name. + * + * If the cache engine & configuration are missing an error will be + * triggered. + * + * @param string $config The configuration name you want an engine for. + * @return \Cake\Cache\CacheEngine When caching is disabled a null engine will be returned. + */ + public static function engine($config) + { + if (!static::$_enabled) { + return new NullEngine(); + } + + $registry = static::getRegistry(); + + if (isset($registry->{$config})) { + return $registry->{$config}; + } + + static::_buildEngine($config); + + return $registry->{$config}; + } + + /** + * Garbage collection + * + * Permanently remove all expired and deleted data + * + * @param string $config [optional] The config name you wish to have garbage collected. Defaults to 'default' + * @param int|null $expires [optional] An expires timestamp. Defaults to NULL + * @return void + */ + public static function gc($config = 'default', $expires = null) + { + $engine = static::engine($config); + $engine->gc($expires); + } + + /** + * Write data for key into cache. + * + * ### Usage: + * + * Writing to the active cache config: + * + * ``` + * Cache::write('cached_data', $data); + * ``` + * + * Writing to a specific cache config: + * + * ``` + * Cache::write('cached_data', $data, 'long_term'); + * ``` + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached - anything except a resource + * @param string $config Optional string configuration name to write to. Defaults to 'default' + * @return bool True if the data was successfully cached, false on failure + */ + public static function write($key, $value, $config = 'default') + { + $engine = static::engine($config); + if (is_resource($value)) { + return false; + } + + $success = $engine->write($key, $value); + if ($success === false && $value !== '') { + trigger_error( + sprintf( + "%s cache was unable to write '%s' to %s cache", + $config, + $key, + get_class($engine) + ), + E_USER_WARNING + ); + } + + return $success; + } + + /** + * Write data for many keys into cache. + * + * ### Usage: + * + * Writing to the active cache config: + * + * ``` + * Cache::writeMany(['cached_data_1' => 'data 1', 'cached_data_2' => 'data 2']); + * ``` + * + * Writing to a specific cache config: + * + * ``` + * Cache::writeMany(['cached_data_1' => 'data 1', 'cached_data_2' => 'data 2'], 'long_term'); + * ``` + * + * @param array $data An array of data to be stored in the cache + * @param string $config Optional string configuration name to write to. Defaults to 'default' + * @return array of bools for each key provided, indicating true for success or false for fail + * @throws \RuntimeException + */ + public static function writeMany($data, $config = 'default') + { + $engine = static::engine($config); + $return = $engine->writeMany($data); + foreach ($return as $key => $success) { + if ($success === false && $data[$key] !== '') { + throw new RuntimeException(sprintf( + '%s cache was unable to write \'%s\' to %s cache', + $config, + $key, + get_class($engine) + )); + } + } + + return $return; + } + + /** + * Read a key from the cache. + * + * ### Usage: + * + * Reading from the active cache configuration. + * + * ``` + * Cache::read('my_data'); + * ``` + * + * Reading from a specific cache configuration. + * + * ``` + * Cache::read('my_data', 'long_term'); + * ``` + * + * @param string $key Identifier for the data + * @param string $config optional name of the configuration to use. Defaults to 'default' + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + public static function read($key, $config = 'default') + { + $engine = static::engine($config); + + return $engine->read($key); + } + + /** + * Read multiple keys from the cache. + * + * ### Usage: + * + * Reading multiple keys from the active cache configuration. + * + * ``` + * Cache::readMany(['my_data_1', 'my_data_2]); + * ``` + * + * Reading from a specific cache configuration. + * + * ``` + * Cache::readMany(['my_data_1', 'my_data_2], 'long_term'); + * ``` + * + * @param array $keys an array of keys to fetch from the cache + * @param string $config optional name of the configuration to use. Defaults to 'default' + * @return array An array containing, for each of the given $keys, the cached data or false if cached data could not be + * retrieved. + */ + public static function readMany($keys, $config = 'default') + { + $engine = static::engine($config); + + return $engine->readMany($keys); + } + + /** + * Increment a number under the key and return incremented value. + * + * @param string $key Identifier for the data + * @param int $offset How much to add + * @param string $config Optional string configuration name. Defaults to 'default' + * @return mixed new value, or false if the data doesn't exist, is not integer, + * or if there was an error fetching it. + */ + public static function increment($key, $offset = 1, $config = 'default') + { + $engine = static::engine($config); + if (!is_int($offset) || $offset < 0) { + return false; + } + + return $engine->increment($key, $offset); + } + + /** + * Decrement a number under the key and return decremented value. + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @param string $config Optional string configuration name. Defaults to 'default' + * @return mixed new value, or false if the data doesn't exist, is not integer, + * or if there was an error fetching it + */ + public static function decrement($key, $offset = 1, $config = 'default') + { + $engine = static::engine($config); + if (!is_int($offset) || $offset < 0) { + return false; + } + + return $engine->decrement($key, $offset); + } + + /** + * Delete a key from the cache. + * + * ### Usage: + * + * Deleting from the active cache configuration. + * + * ``` + * Cache::delete('my_data'); + * ``` + * + * Deleting from a specific cache configuration. + * + * ``` + * Cache::delete('my_data', 'long_term'); + * ``` + * + * @param string $key Identifier for the data + * @param string $config name of the configuration to use. Defaults to 'default' + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public static function delete($key, $config = 'default') + { + $engine = static::engine($config); + + return $engine->delete($key); + } + + /** + * Delete many keys from the cache. + * + * ### Usage: + * + * Deleting multiple keys from the active cache configuration. + * + * ``` + * Cache::deleteMany(['my_data_1', 'my_data_2']); + * ``` + * + * Deleting from a specific cache configuration. + * + * ``` + * Cache::deleteMany(['my_data_1', 'my_data_2], 'long_term'); + * ``` + * + * @param array $keys Array of cache keys to be deleted + * @param string $config name of the configuration to use. Defaults to 'default' + * @return array of boolean values that are true if the value was successfully deleted, false if it didn't exist or + * couldn't be removed + */ + public static function deleteMany($keys, $config = 'default') + { + $engine = static::engine($config); + + return $engine->deleteMany($keys); + } + + /** + * Delete all keys from the cache. + * + * @param bool $check if true will check expiration, otherwise delete all + * @param string $config name of the configuration to use. Defaults to 'default' + * @return bool True if the cache was successfully cleared, false otherwise + */ + public static function clear($check = false, $config = 'default') + { + $engine = static::engine($config); + + return $engine->clear($check); + } + + /** + * Delete all keys from the cache from all configurations. + * + * @param bool $check if true will check expiration, otherwise delete all + * @return array Status code. For each configuration, it reports the status of the operation + */ + public static function clearAll($check = false) + { + $status = []; + + foreach (self::configured() as $config) { + $status[$config] = self::clear($check, $config); + } + + return $status; + } + + /** + * Delete all keys from the cache belonging to the same group. + * + * @param string $group name of the group to be cleared + * @param string $config name of the configuration to use. Defaults to 'default' + * @return bool True if the cache group was successfully cleared, false otherwise + */ + public static function clearGroup($group, $config = 'default') + { + $engine = static::engine($config); + + return $engine->clearGroup($group); + } + + /** + * Retrieve group names to config mapping. + * + * ``` + * Cache::config('daily', ['duration' => '1 day', 'groups' => ['posts']]); + * Cache::config('weekly', ['duration' => '1 week', 'groups' => ['posts', 'archive']]); + * $configs = Cache::groupConfigs('posts'); + * ``` + * + * $configs will equal to `['posts' => ['daily', 'weekly']]` + * Calling this method will load all the configured engines. + * + * @param string|null $group group name or null to retrieve all group mappings + * @return array map of group and all configuration that has the same group + * @throws \InvalidArgumentException + */ + public static function groupConfigs($group = null) + { + foreach (array_keys(static::$_config) as $config) { + static::engine($config); + } + if ($group === null) { + return static::$_groups; + } + + if (isset(self::$_groups[$group])) { + return [$group => self::$_groups[$group]]; + } + + throw new InvalidArgumentException(sprintf('Invalid cache group %s', $group)); + } + + /** + * Re-enable caching. + * + * If caching has been disabled with Cache::disable() this method will reverse that effect. + * + * @return void + */ + public static function enable() + { + static::$_enabled = true; + } + + /** + * Disable caching. + * + * When disabled all cache operations will return null. + * + * @return void + */ + public static function disable() + { + static::$_enabled = false; + } + + /** + * Check whether or not caching is enabled. + * + * @return bool + */ + public static function enabled() + { + return static::$_enabled; + } + + /** + * Provides the ability to easily do read-through caching. + * + * When called if the $key is not set in $config, the $callable function + * will be invoked. The results will then be stored into the cache config + * at key. + * + * Examples: + * + * Using a Closure to provide data, assume `$this` is a Table object: + * + * ``` + * $results = Cache::remember('all_articles', function () { + * return $this->find('all'); + * }); + * ``` + * + * @param string $key The cache key to read/store data at. + * @param callable $callable The callable that provides data in the case when + * the cache key is empty. Can be any callable type supported by your PHP. + * @param string $config The cache configuration to use for this operation. + * Defaults to default. + * @return mixed If the key is found: the cached data, false if the data + * missing/expired, or an error. If the key is not found: boolean of the + * success of the write + */ + public static function remember($key, $callable, $config = 'default') + { + $existing = self::read($key, $config); + if ($existing !== false) { + return $existing; + } + $results = call_user_func($callable); + self::write($key, $results, $config); + + return $results; + } + + /** + * Write data for key into a cache engine if it doesn't exist already. + * + * ### Usage: + * + * Writing to the active cache config: + * + * ``` + * Cache::add('cached_data', $data); + * ``` + * + * Writing to a specific cache config: + * + * ``` + * Cache::add('cached_data', $data, 'long_term'); + * ``` + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached - anything except a resource. + * @param string $config Optional string configuration name to write to. Defaults to 'default'. + * @return bool True if the data was successfully cached, false on failure. + * Or if the key existed already. + */ + public static function add($key, $value, $config = 'default') + { + $engine = static::engine($config); + if (is_resource($value)) { + return false; + } + + return $engine->add($key, $value); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Cache/CacheEngine.php b/app/vendor/cakephp/cakephp/src/Cache/CacheEngine.php new file mode 100644 index 000000000..32658f09f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Cache/CacheEngine.php @@ -0,0 +1,298 @@ + 3600, + 'groups' => [], + 'prefix' => 'cake_', + 'probability' => 100, + 'warnOnWriteFailures' => true, + ]; + + /** + * Contains the compiled string with all groups + * prefixes to be prepended to every key in this cache engine + * + * @var string + */ + protected $_groupPrefix; + + /** + * Initialize the cache engine + * + * Called automatically by the cache frontend. Merge the runtime config with the defaults + * before use. + * + * @param array $config Associative array of parameters for the engine + * @return bool True if the engine has been successfully initialized, false if not + */ + public function init(array $config = []) + { + $this->setConfig($config); + + if (!empty($this->_config['groups'])) { + sort($this->_config['groups']); + $this->_groupPrefix = str_repeat('%s_', count($this->_config['groups'])); + } + if (!is_numeric($this->_config['duration'])) { + $this->_config['duration'] = strtotime($this->_config['duration']) - time(); + } + + return true; + } + + /** + * Garbage collection + * + * Permanently remove all expired and deleted data + * + * @param int|null $expires [optional] An expires timestamp, invalidating all data before. + * @return void + */ + public function gc($expires = null) + { + } + + /** + * Write value for a key into cache + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @return bool True if the data was successfully cached, false on failure + */ + abstract public function write($key, $value); + + /** + * Write data for many keys into cache + * + * @param array $data An array of data to be stored in the cache + * @return array of bools for each key provided, true if the data was successfully cached, false on failure + */ + public function writeMany($data) + { + $return = []; + foreach ($data as $key => $value) { + $return[$key] = $this->write($key, $value); + } + + return $return; + } + + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + abstract public function read($key); + + /** + * Read multiple keys from the cache + * + * @param array $keys An array of identifiers for the data + * @return array For each cache key (given as the array key) the cache data associated or false if the data doesn't + * exist, has expired, or if there was an error fetching it + */ + public function readMany($keys) + { + $return = []; + foreach ($keys as $key) { + $return[$key] = $this->read($key); + } + + return $return; + } + + /** + * Increment a number under the key and return incremented value + * + * @param string $key Identifier for the data + * @param int $offset How much to add + * @return bool|int New incremented value, false otherwise + */ + abstract public function increment($key, $offset = 1); + + /** + * Decrement a number under the key and return decremented value + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return bool|int New incremented value, false otherwise + */ + abstract public function decrement($key, $offset = 1); + + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + abstract public function delete($key); + + /** + * Delete all keys from the cache + * + * @param bool $check if true will check expiration, otherwise delete all + * @return bool True if the cache was successfully cleared, false otherwise + */ + abstract public function clear($check); + + /** + * Deletes keys from the cache + * + * @param array $keys An array of identifiers for the data + * @return array For each provided cache key (given back as the array key) true if the value was successfully deleted, + * false if it didn't exist or couldn't be removed + */ + public function deleteMany($keys) + { + $return = []; + foreach ($keys as $key) { + $return[$key] = $this->delete($key); + } + + return $return; + } + + /** + * Add a key to the cache if it does not already exist. + * + * Defaults to a non-atomic implementation. Subclasses should + * prefer atomic implementations. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @return bool True if the data was successfully cached, false on failure. + */ + public function add($key, $value) + { + $cachedValue = $this->read($key); + if ($cachedValue === false) { + return $this->write($key, $value); + } + + return false; + } + + /** + * Clears all values belonging to a group. Is up to the implementing engine + * to decide whether actually delete the keys or just simulate it to achieve + * the same result. + * + * @param string $group name of the group to be cleared + * @return bool + */ + public function clearGroup($group) + { + return false; + } + + /** + * Does whatever initialization for each group is required + * and returns the `group value` for each of them, this is + * the token representing each group in the cache key + * + * @return array + */ + public function groups() + { + return $this->_config['groups']; + } + + /** + * Generates a safe key for use with cache engine storage engines. + * + * @param string $key the key passed over + * @return bool|string string key or false + */ + public function key($key) + { + if (!$key) { + return false; + } + + $prefix = ''; + if ($this->_groupPrefix) { + $prefix = md5(implode('_', $this->groups())); + } + + $key = preg_replace('/[\s]+/', '_', strtolower(trim(str_replace([DIRECTORY_SEPARATOR, '/', '.'], '_', (string)$key)))); + + return $prefix . $key; + } + + /** + * Generates a safe key, taking account of the configured key prefix + * + * @param string $key the key passed over + * @return mixed string $key or false + * @throws \InvalidArgumentException If key's value is empty + */ + protected function _key($key) + { + $key = $this->key($key); + if ($key === false) { + throw new InvalidArgumentException('An empty value is not valid as a cache key'); + } + + return $this->_config['prefix'] . $key; + } + + /** + * Cache Engines may trigger warnings if they encounter failures during operation, + * if option warnOnWriteFailures is set to true. + * + * @param string $message The warning message. + * @return void + */ + protected function warning($message) + { + if ($this->getConfig('warnOnWriteFailures') !== true) { + return; + } + + triggerWarning($message); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Cache/CacheRegistry.php b/app/vendor/cakephp/cakephp/src/Cache/CacheRegistry.php new file mode 100644 index 000000000..706632f11 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Cache/CacheRegistry.php @@ -0,0 +1,114 @@ +init($config)) { + throw new RuntimeException( + sprintf('Cache engine %s is not properly configured.', get_class($instance)) + ); + } + + $config = $instance->getConfig(); + if ($config['probability'] && time() % $config['probability'] === 0) { + $instance->gc(); + } + + return $instance; + } + + /** + * Remove a single adapter from the registry. + * + * @param string $name The adapter name. + * @return void + */ + public function unload($name) + { + unset($this->_loaded[$name]); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Cache/Engine/ApcEngine.php b/app/vendor/cakephp/cakephp/src/Cache/Engine/ApcEngine.php new file mode 100644 index 000000000..982ed3113 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Cache/Engine/ApcEngine.php @@ -0,0 +1,20 @@ +_key($key); + $duration = $this->_config['duration']; + + return apcu_store($key, $value, $duration); + } + + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, + * has expired, or if there was an error fetching it + * @link https://secure.php.net/manual/en/function.apcu-fetch.php + */ + public function read($key) + { + $key = $this->_key($key); + + return apcu_fetch($key); + } + + /** + * Increments the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to increment + * @return bool|int New incremented value, false otherwise + * @link https://secure.php.net/manual/en/function.apcu-inc.php + */ + public function increment($key, $offset = 1) + { + $key = $this->_key($key); + + return apcu_inc($key, $offset); + } + + /** + * Decrements the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return bool|int New decremented value, false otherwise + * @link https://secure.php.net/manual/en/function.apcu-dec.php + */ + public function decrement($key, $offset = 1) + { + $key = $this->_key($key); + + return apcu_dec($key, $offset); + } + + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + * @link https://secure.php.net/manual/en/function.apcu-delete.php + */ + public function delete($key) + { + $key = $this->_key($key); + + return apcu_delete($key); + } + + /** + * Delete all keys from the cache. This will clear every cache config using APC. + * + * @param bool $check If true, nothing will be cleared, as entries are removed + * from APC as they expired. This flag is really only used by FileEngine. + * @return bool True Returns true. + * @link https://secure.php.net/manual/en/function.apcu-cache-info.php + * @link https://secure.php.net/manual/en/function.apcu-delete.php + */ + public function clear($check) + { + if ($check) { + return true; + } + if (class_exists('APCuIterator', false)) { + $iterator = new APCuIterator( + '/^' . preg_quote($this->_config['prefix'], '/') . '/', + APC_ITER_NONE + ); + apcu_delete($iterator); + + return true; + } + + $cache = apcu_cache_info(); // Raises warning by itself already + foreach ($cache['cache_list'] as $key) { + if (strpos($key['info'], $this->_config['prefix']) === 0) { + apcu_delete($key['info']); + } + } + + return true; + } + + /** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @return bool True if the data was successfully cached, false on failure. + * @link https://secure.php.net/manual/en/function.apcu-add.php + */ + public function add($key, $value) + { + $key = $this->_key($key); + $duration = $this->_config['duration']; + + return apcu_add($key, $value, $duration); + } + + /** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + * @link https://secure.php.net/manual/en/function.apcu-fetch.php + * @link https://secure.php.net/manual/en/function.apcu-store.php + */ + public function groups() + { + if (empty($this->_compiledGroupNames)) { + foreach ($this->_config['groups'] as $group) { + $this->_compiledGroupNames[] = $this->_config['prefix'] . $group; + } + } + + $success = false; + $groups = apcu_fetch($this->_compiledGroupNames, $success); + if ($success && count($groups) !== count($this->_config['groups'])) { + foreach ($this->_compiledGroupNames as $group) { + if (!isset($groups[$group])) { + $value = 1; + if (apcu_store($group, $value) === false) { + $this->warning( + sprintf('Failed to store key "%s" with value "%s" into APCu cache.', $group, $value) + ); + } + $groups[$group] = $value; + } + } + ksort($groups); + } + + $result = []; + $groups = array_values($groups); + foreach ($this->_config['groups'] as $i => $group) { + $result[] = $group . $groups[$i]; + } + + return $result; + } + + /** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @param string $group The group to clear. + * @return bool success + * @link https://secure.php.net/manual/en/function.apcu-inc.php + */ + public function clearGroup($group) + { + $success = false; + apcu_inc($this->_config['prefix'] . $group, 1, $success); + + return $success; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Cache/Engine/FileEngine.php b/app/vendor/cakephp/cakephp/src/Cache/Engine/FileEngine.php new file mode 100644 index 000000000..a5367f373 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Cache/Engine/FileEngine.php @@ -0,0 +1,495 @@ + 3600, + 'groups' => [], + 'isWindows' => false, + 'lock' => true, + 'mask' => 0664, + 'path' => null, + 'prefix' => 'cake_', + 'probability' => 100, + 'serialize' => true + ]; + + /** + * True unless FileEngine::__active(); fails + * + * @var bool + */ + protected $_init = true; + + /** + * Initialize File Cache Engine + * + * Called automatically by the cache frontend. + * + * @param array $config array of setting for the engine + * @return bool True if the engine has been successfully initialized, false if not + */ + public function init(array $config = []) + { + parent::init($config); + + if ($this->_config['path'] === null) { + $this->_config['path'] = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'cake_cache' . DIRECTORY_SEPARATOR; + } + if (DIRECTORY_SEPARATOR === '\\') { + $this->_config['isWindows'] = true; + } + if (substr($this->_config['path'], -1) !== DIRECTORY_SEPARATOR) { + $this->_config['path'] .= DIRECTORY_SEPARATOR; + } + if ($this->_groupPrefix) { + $this->_groupPrefix = str_replace('_', DIRECTORY_SEPARATOR, $this->_groupPrefix); + } + + return $this->_active(); + } + + /** + * Garbage collection. Permanently remove all expired and deleted data + * + * @param int|null $expires [optional] An expires timestamp, invalidating all data before. + * @return bool True if garbage collection was successful, false on failure + */ + public function gc($expires = null) + { + return $this->clear(true); + } + + /** + * Write data for key into cache + * + * @param string $key Identifier for the data + * @param mixed $data Data to be cached + * @return bool True if the data was successfully cached, false on failure + */ + public function write($key, $data) + { + if ($data === '' || !$this->_init) { + return false; + } + + $key = $this->_key($key); + + if ($this->_setKey($key, true) === false) { + return false; + } + + $lineBreak = "\n"; + + if ($this->_config['isWindows']) { + $lineBreak = "\r\n"; + } + + if (!empty($this->_config['serialize'])) { + if ($this->_config['isWindows']) { + $data = str_replace('\\', '\\\\\\\\', serialize($data)); + } else { + $data = serialize($data); + } + } + + $duration = $this->_config['duration']; + $expires = time() + $duration; + $contents = implode([$expires, $lineBreak, $data, $lineBreak]); + + if ($this->_config['lock']) { + $this->_File->flock(LOCK_EX); + } + + $this->_File->rewind(); + $success = $this->_File->ftruncate(0) && + $this->_File->fwrite($contents) && + $this->_File->fflush(); + + if ($this->_config['lock']) { + $this->_File->flock(LOCK_UN); + } + $this->_File = null; + + return $success; + } + + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has + * expired, or if there was an error fetching it + */ + public function read($key) + { + $key = $this->_key($key); + + if (!$this->_init || $this->_setKey($key) === false) { + return false; + } + + if ($this->_config['lock']) { + $this->_File->flock(LOCK_SH); + } + + $this->_File->rewind(); + $time = time(); + $cachetime = (int)$this->_File->current(); + + if ($cachetime < $time || ($time + $this->_config['duration']) < $cachetime) { + if ($this->_config['lock']) { + $this->_File->flock(LOCK_UN); + } + + return false; + } + + $data = ''; + $this->_File->next(); + while ($this->_File->valid()) { + $data .= $this->_File->current(); + $this->_File->next(); + } + + if ($this->_config['lock']) { + $this->_File->flock(LOCK_UN); + } + + $data = trim($data); + + if ($data !== '' && !empty($this->_config['serialize'])) { + if ($this->_config['isWindows']) { + $data = str_replace('\\\\\\\\', '\\', $data); + } + $data = unserialize((string)$data); + } + + return $data; + } + + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't + * exist or couldn't be removed + */ + public function delete($key) + { + $key = $this->_key($key); + + if ($this->_setKey($key) === false || !$this->_init) { + return false; + } + + $path = $this->_File->getRealPath(); + $this->_File = null; + + //@codingStandardsIgnoreStart + return @unlink($path); + //@codingStandardsIgnoreEnd + } + + /** + * Delete all values from the cache + * + * @param bool $check Optional - only delete expired cache items + * @return bool True if the cache was successfully cleared, false otherwise + */ + public function clear($check) + { + if (!$this->_init) { + return false; + } + $this->_File = null; + + $threshold = $now = false; + if ($check) { + $now = time(); + $threshold = $now - $this->_config['duration']; + } + + $this->_clearDirectory($this->_config['path'], $now, $threshold); + + $directory = new RecursiveDirectoryIterator($this->_config['path']); + $contents = new RecursiveIteratorIterator( + $directory, + RecursiveIteratorIterator::SELF_FIRST + ); + $cleared = []; + foreach ($contents as $path) { + if ($path->isFile()) { + continue; + } + + $path = $path->getRealPath() . DIRECTORY_SEPARATOR; + if (!in_array($path, $cleared)) { + $this->_clearDirectory($path, $now, $threshold); + $cleared[] = $path; + } + } + + return true; + } + + /** + * Used to clear a directory of matching files. + * + * @param string $path The path to search. + * @param int $now The current timestamp + * @param int $threshold Any file not modified after this value will be deleted. + * @return void + */ + protected function _clearDirectory($path, $now, $threshold) + { + if (!is_dir($path)) { + return; + } + $prefixLength = strlen($this->_config['prefix']); + + $dir = dir($path); + while (($entry = $dir->read()) !== false) { + if (substr($entry, 0, $prefixLength) !== $this->_config['prefix']) { + continue; + } + + try { + $file = new SplFileObject($path . $entry, 'r'); + } catch (Exception $e) { + continue; + } + + if ($threshold) { + $mtime = $file->getMTime(); + if ($mtime > $threshold) { + continue; + } + + $expires = (int)$file->current(); + if ($expires > $now) { + continue; + } + } + if ($file->isFile()) { + $filePath = $file->getRealPath(); + $file = null; + + //@codingStandardsIgnoreStart + @unlink($filePath); + //@codingStandardsIgnoreEnd + } + } + } + + /** + * Not implemented + * + * @param string $key The key to decrement + * @param int $offset The number to offset + * @return void + * @throws \LogicException + */ + public function decrement($key, $offset = 1) + { + throw new LogicException('Files cannot be atomically decremented.'); + } + + /** + * Not implemented + * + * @param string $key The key to increment + * @param int $offset The number to offset + * @return void + * @throws \LogicException + */ + public function increment($key, $offset = 1) + { + throw new LogicException('Files cannot be atomically incremented.'); + } + + /** + * Sets the current cache key this class is managing, and creates a writable SplFileObject + * for the cache file the key is referring to. + * + * @param string $key The key + * @param bool $createKey Whether the key should be created if it doesn't exists, or not + * @return bool true if the cache key could be set, false otherwise + */ + protected function _setKey($key, $createKey = false) + { + $groups = null; + if ($this->_groupPrefix) { + $groups = vsprintf($this->_groupPrefix, $this->groups()); + } + $dir = $this->_config['path'] . $groups; + + if (!is_dir($dir)) { + mkdir($dir, 0775, true); + } + + $path = new SplFileInfo($dir . $key); + + if (!$createKey && !$path->isFile()) { + return false; + } + if (empty($this->_File) || $this->_File->getBasename() !== $key) { + $exists = file_exists($path->getPathname()); + try { + $this->_File = $path->openFile('c+'); + } catch (Exception $e) { + trigger_error($e->getMessage(), E_USER_WARNING); + + return false; + } + unset($path); + + if (!$exists && !chmod($this->_File->getPathname(), (int)$this->_config['mask'])) { + trigger_error(sprintf( + 'Could not apply permission mask "%s" on cache file "%s"', + $this->_File->getPathname(), + $this->_config['mask'] + ), E_USER_WARNING); + } + } + + return true; + } + + /** + * Determine is cache directory is writable + * + * @return bool + */ + protected function _active() + { + $dir = new SplFileInfo($this->_config['path']); + $path = $dir->getPathname(); + $success = true; + if (!is_dir($path)) { + //@codingStandardsIgnoreStart + $success = @mkdir($path, 0775, true); + //@codingStandardsIgnoreEnd + } + + $isWritableDir = ($dir->isDir() && $dir->isWritable()); + if (!$success || ($this->_init && !$isWritableDir)) { + $this->_init = false; + trigger_error(sprintf( + '%s is not writable', + $this->_config['path'] + ), E_USER_WARNING); + } + + return $success; + } + + /** + * Generates a safe key for use with cache engine storage engines. + * + * @param string $key the key passed over + * @return mixed string $key or false + */ + public function key($key) + { + if (empty($key)) { + return false; + } + + $key = Inflector::underscore(str_replace( + [DIRECTORY_SEPARATOR, '/', '.', '<', '>', '?', ':', '|', '*', '"'], + '_', + (string)$key + )); + + return $key; + } + + /** + * Recursively deletes all files under any directory named as $group + * + * @param string $group The group to clear. + * @return bool success + */ + public function clearGroup($group) + { + $this->_File = null; + $directoryIterator = new RecursiveDirectoryIterator($this->_config['path']); + $contents = new RecursiveIteratorIterator( + $directoryIterator, + RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($contents as $object) { + $containsGroup = strpos($object->getPathname(), DIRECTORY_SEPARATOR . $group . DIRECTORY_SEPARATOR) !== false; + $hasPrefix = true; + if (strlen($this->_config['prefix']) !== 0) { + $hasPrefix = strpos($object->getBasename(), $this->_config['prefix']) === 0; + } + if ($object->isFile() && $containsGroup && $hasPrefix) { + $path = $object->getPathname(); + $object = null; + //@codingStandardsIgnoreStart + @unlink($path); + //@codingStandardsIgnoreEnd + } + } + + return true; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Cache/Engine/MemcachedEngine.php b/app/vendor/cakephp/cakephp/src/Cache/Engine/MemcachedEngine.php new file mode 100644 index 000000000..8e6b1c56e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Cache/Engine/MemcachedEngine.php @@ -0,0 +1,534 @@ + value. + * Use the \Memcached::OPT_* constants as keys. + * + * @var array + */ + protected $_defaultConfig = [ + 'compress' => false, + 'duration' => 3600, + 'groups' => [], + 'host' => null, + 'username' => null, + 'password' => null, + 'persistent' => false, + 'port' => null, + 'prefix' => 'cake_', + 'probability' => 100, + 'serialize' => 'php', + 'servers' => ['127.0.0.1'], + 'options' => [], + ]; + + /** + * List of available serializer engines + * + * Memcached must be compiled with json and igbinary support to use these engines + * + * @var array + */ + protected $_serializers = []; + + /** + * @var string[] + */ + protected $_compiledGroupNames = []; + + /** + * Initialize the Cache Engine + * + * Called automatically by the cache frontend + * + * @param array $config array of setting for the engine + * @return bool True if the engine has been successfully initialized, false if not + * @throws \InvalidArgumentException When you try use authentication without + * Memcached compiled with SASL support + */ + public function init(array $config = []) + { + if (!extension_loaded('memcached')) { + return false; + } + + $this->_serializers = [ + 'igbinary' => Memcached::SERIALIZER_IGBINARY, + 'json' => Memcached::SERIALIZER_JSON, + 'php' => Memcached::SERIALIZER_PHP + ]; + if (defined('Memcached::HAVE_MSGPACK') && Memcached::HAVE_MSGPACK) { + $this->_serializers['msgpack'] = Memcached::SERIALIZER_MSGPACK; + } + + parent::init($config); + + if (!empty($config['host'])) { + if (empty($config['port'])) { + $config['servers'] = [$config['host']]; + } else { + $config['servers'] = [sprintf('%s:%d', $config['host'], $config['port'])]; + } + } + + if (isset($config['servers'])) { + $this->setConfig('servers', $config['servers'], false); + } + + if (!is_array($this->_config['servers'])) { + $this->_config['servers'] = [$this->_config['servers']]; + } + + if (isset($this->_Memcached)) { + return true; + } + + if ($this->_config['persistent']) { + $this->_Memcached = new Memcached((string)$this->_config['persistent']); + } else { + $this->_Memcached = new Memcached(); + } + $this->_setOptions(); + + if (count($this->_Memcached->getServerList())) { + return true; + } + + $servers = []; + foreach ($this->_config['servers'] as $server) { + $servers[] = $this->parseServerString($server); + } + + if (!$this->_Memcached->addServers($servers)) { + return false; + } + + if (is_array($this->_config['options'])) { + foreach ($this->_config['options'] as $opt => $value) { + $this->_Memcached->setOption($opt, $value); + } + } + + if (empty($this->_config['username']) && !empty($this->_config['login'])) { + throw new InvalidArgumentException( + 'Please pass "username" instead of "login" for connecting to Memcached' + ); + } + + if ($this->_config['username'] !== null && $this->_config['password'] !== null) { + if (!method_exists($this->_Memcached, 'setSaslAuthData')) { + throw new InvalidArgumentException( + 'Memcached extension is not built with SASL support' + ); + } + $this->_Memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); + $this->_Memcached->setSaslAuthData( + $this->_config['username'], + $this->_config['password'] + ); + } + + return true; + } + + /** + * Settings the memcached instance + * + * @return void + * @throws \InvalidArgumentException When the Memcached extension is not built + * with the desired serializer engine. + */ + protected function _setOptions() + { + $this->_Memcached->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); + + $serializer = strtolower($this->_config['serialize']); + if (!isset($this->_serializers[$serializer])) { + throw new InvalidArgumentException( + sprintf('%s is not a valid serializer engine for Memcached', $serializer) + ); + } + + if ($serializer !== 'php' && + !constant('Memcached::HAVE_' . strtoupper($serializer)) + ) { + throw new InvalidArgumentException( + sprintf('Memcached extension is not compiled with %s support', $serializer) + ); + } + + $this->_Memcached->setOption( + Memcached::OPT_SERIALIZER, + $this->_serializers[$serializer] + ); + + // Check for Amazon ElastiCache instance + if (defined('Memcached::OPT_CLIENT_MODE') && + defined('Memcached::DYNAMIC_CLIENT_MODE') + ) { + $this->_Memcached->setOption( + Memcached::OPT_CLIENT_MODE, + Memcached::DYNAMIC_CLIENT_MODE + ); + } + + $this->_Memcached->setOption( + Memcached::OPT_COMPRESSION, + (bool)$this->_config['compress'] + ); + } + + /** + * Parses the server address into the host/port. Handles both IPv6 and IPv4 + * addresses and Unix sockets + * + * @param string $server The server address string. + * @return array Array containing host, port + */ + public function parseServerString($server) + { + $socketTransport = 'unix://'; + if (strpos($server, $socketTransport) === 0) { + return [substr($server, strlen($socketTransport)), 0]; + } + if (substr($server, 0, 1) === '[') { + $position = strpos($server, ']:'); + if ($position !== false) { + $position++; + } + } else { + $position = strpos($server, ':'); + } + $port = 11211; + $host = $server; + if ($position !== false) { + $host = substr($server, 0, $position); + $port = substr($server, $position + 1); + } + + return [$host, (int)$port]; + } + + /** + * Backwards compatible alias of parseServerString + * + * @param string $server The server address string. + * @return array Array containing host, port + * @deprecated 3.4.13 Will be removed in 4.0.0 + */ + protected function _parseServerString($server) + { + return $this->parseServerString($server); + } + + /** + * Read an option value from the memcached connection. + * + * @param string $name The option name to read. + * @return string|int|null|bool + */ + public function getOption($name) + { + return $this->_Memcached->getOption($name); + } + + /** + * Write data for key into cache. When using memcached as your cache engine + * remember that the Memcached pecl extension does not support cache expiry + * times greater than 30 days in the future. Any duration greater than 30 days + * will be treated as never expiring. + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @return bool True if the data was successfully cached, false on failure + * @see https://secure.php.net/manual/en/memcache.set.php + */ + public function write($key, $value) + { + $duration = $this->_config['duration']; + if ($duration > 30 * DAY) { + $duration = 0; + } + + $key = $this->_key($key); + + return $this->_Memcached->set($key, $value, $duration); + } + + /** + * Write many cache entries to the cache at once + * + * @param array $data An array of data to be stored in the cache + * @return array of bools for each key provided, true if the data was + * successfully cached, false on failure + */ + public function writeMany($data) + { + $cacheData = []; + foreach ($data as $key => $value) { + $cacheData[$this->_key($key)] = $value; + } + + $success = $this->_Memcached->setMulti($cacheData); + + $return = []; + foreach (array_keys($data) as $key) { + $return[$key] = $success; + } + + return $return; + } + + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has + * expired, or if there was an error fetching it. + */ + public function read($key) + { + $key = $this->_key($key); + + return $this->_Memcached->get($key); + } + + /** + * Read many keys from the cache at once + * + * @param array $keys An array of identifiers for the data + * @return array An array containing, for each of the given $keys, the cached data or + * false if cached data could not be retrieved. + */ + public function readMany($keys) + { + $cacheKeys = []; + foreach ($keys as $key) { + $cacheKeys[] = $this->_key($key); + } + + $values = $this->_Memcached->getMulti($cacheKeys); + $return = []; + foreach ($keys as &$key) { + $return[$key] = array_key_exists($this->_key($key), $values) ? + $values[$this->_key($key)] : false; + } + + return $return; + } + + /** + * Increments the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to increment + * @return bool|int New incremented value, false otherwise + */ + public function increment($key, $offset = 1) + { + $key = $this->_key($key); + + return $this->_Memcached->increment($key, $offset); + } + + /** + * Decrements the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return bool|int New decremented value, false otherwise + */ + public function decrement($key, $offset = 1) + { + $key = $this->_key($key); + + return $this->_Memcached->decrement($key, $offset); + } + + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't + * exist or couldn't be removed. + */ + public function delete($key) + { + $key = $this->_key($key); + + return $this->_Memcached->delete($key); + } + + /** + * Delete many keys from the cache at once + * + * @param array $keys An array of identifiers for the data + * @return array of boolean values that are true if the key was successfully + * deleted, false if it didn't exist or couldn't be removed. + */ + public function deleteMany($keys) + { + $cacheKeys = []; + foreach ($keys as $key) { + $cacheKeys[] = $this->_key($key); + } + + $success = $this->_Memcached->deleteMulti($cacheKeys); + + $return = []; + foreach ($keys as $key) { + $return[$key] = $success; + } + + return $return; + } + + /** + * Delete all keys from the cache + * + * @param bool $check If true will check expiration, otherwise delete all. + * @return bool True if the cache was successfully cleared, false otherwise + */ + public function clear($check) + { + if ($check) { + return true; + } + + $keys = $this->_Memcached->getAllKeys(); + if ($keys === false) { + return false; + } + + foreach ($keys as $key) { + if (strpos($key, $this->_config['prefix']) === 0) { + $this->_Memcached->delete($key); + } + } + + return true; + } + + /** + * Add a key to the cache if it does not already exist. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @return bool True if the data was successfully cached, false on failure. + */ + public function add($key, $value) + { + $duration = $this->_config['duration']; + if ($duration > 30 * DAY) { + $duration = 0; + } + + $key = $this->_key($key); + + return $this->_Memcached->add($key, $value, $duration); + } + + /** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + */ + public function groups() + { + if (empty($this->_compiledGroupNames)) { + foreach ($this->_config['groups'] as $group) { + $this->_compiledGroupNames[] = $this->_config['prefix'] . $group; + } + } + + $groups = $this->_Memcached->getMulti($this->_compiledGroupNames) ?: []; + if (count($groups) !== count($this->_config['groups'])) { + foreach ($this->_compiledGroupNames as $group) { + if (!isset($groups[$group])) { + $this->_Memcached->set($group, 1, 0); + $groups[$group] = 1; + } + } + ksort($groups); + } + + $result = []; + $groups = array_values($groups); + foreach ($this->_config['groups'] as $i => $group) { + $result[] = $group . $groups[$i]; + } + + return $result; + } + + /** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @param string $group name of the group to be cleared + * @return bool success + */ + public function clearGroup($group) + { + return (bool)$this->_Memcached->increment($this->_config['prefix'] . $group); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Cache/Engine/NullEngine.php b/app/vendor/cakephp/cakephp/src/Cache/Engine/NullEngine.php new file mode 100644 index 000000000..83ba3cde2 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Cache/Engine/NullEngine.php @@ -0,0 +1,116 @@ + 0, + 'duration' => 3600, + 'groups' => [], + 'password' => false, + 'persistent' => true, + 'port' => 6379, + 'prefix' => 'cake_', + 'probability' => 100, + 'host' => null, + 'server' => '127.0.0.1', + 'timeout' => 0, + 'unix_socket' => false, + ]; + + /** + * Initialize the Cache Engine + * + * Called automatically by the cache frontend + * + * @param array $config array of setting for the engine + * @return bool True if the engine has been successfully initialized, false if not + */ + public function init(array $config = []) + { + if (!extension_loaded('redis')) { + return false; + } + + if (!empty($config['host'])) { + $config['server'] = $config['host']; + } + + parent::init($config); + + return $this->_connect(); + } + + /** + * Connects to a Redis server + * + * @return bool True if Redis server was connected + */ + protected function _connect() + { + try { + $this->_Redis = new Redis(); + if (!empty($this->_config['unix_socket'])) { + $return = $this->_Redis->connect($this->_config['unix_socket']); + } elseif (empty($this->_config['persistent'])) { + $return = $this->_Redis->connect($this->_config['server'], $this->_config['port'], $this->_config['timeout']); + } else { + $persistentId = $this->_config['port'] . $this->_config['timeout'] . $this->_config['database']; + $return = $this->_Redis->pconnect($this->_config['server'], $this->_config['port'], $this->_config['timeout'], $persistentId); + } + } catch (RedisException $e) { + return false; + } + if ($return && $this->_config['password']) { + $return = $this->_Redis->auth($this->_config['password']); + } + if ($return) { + $return = $this->_Redis->select($this->_config['database']); + } + + return $return; + } + + /** + * Write data for key into cache. + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @return bool True if the data was successfully cached, false on failure + */ + public function write($key, $value) + { + $key = $this->_key($key); + + if (!is_int($value)) { + $value = serialize($value); + } + + $duration = $this->_config['duration']; + if ($duration === 0) { + return $this->_Redis->set($key, $value); + } + + return $this->_Redis->setex($key, $duration, $value); + } + + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + public function read($key) + { + $key = $this->_key($key); + + $value = $this->_Redis->get($key); + if (preg_match('/^[-]?\d+$/', $value)) { + return (int)$value; + } + if ($value !== false && is_string($value)) { + return unserialize($value); + } + + return $value; + } + + /** + * Increments the value of an integer cached key & update the expiry time + * + * @param string $key Identifier for the data + * @param int $offset How much to increment + * @return bool|int New incremented value, false otherwise + */ + public function increment($key, $offset = 1) + { + $duration = $this->_config['duration']; + $key = $this->_key($key); + + $value = (int)$this->_Redis->incrBy($key, $offset); + if ($duration > 0) { + $this->_Redis->setTimeout($key, $duration); + } + + return $value; + } + + /** + * Decrements the value of an integer cached key & update the expiry time + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return bool|int New decremented value, false otherwise + */ + public function decrement($key, $offset = 1) + { + $duration = $this->_config['duration']; + $key = $this->_key($key); + + $value = (int)$this->_Redis->decrBy($key, $offset); + if ($duration > 0) { + $this->_Redis->setTimeout($key, $duration); + } + + return $value; + } + + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) + { + $key = $this->_key($key); + + return $this->_Redis->delete($key) > 0; + } + + /** + * Delete all keys from the cache + * + * @param bool $check If true will check expiration, otherwise delete all. + * @return bool True if the cache was successfully cleared, false otherwise + */ + public function clear($check) + { + if ($check) { + return true; + } + $keys = $this->_Redis->getKeys($this->_config['prefix'] . '*'); + + $result = []; + foreach ($keys as $key) { + $result[] = $this->_Redis->delete($key) > 0; + } + + return !in_array(false, $result); + } + + /** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @return bool True if the data was successfully cached, false on failure. + * @link https://github.com/phpredis/phpredis#setnx + */ + public function add($key, $value) + { + $duration = $this->_config['duration']; + $key = $this->_key($key); + + if (!is_int($value)) { + $value = serialize($value); + } + + // setnx() doesn't have an expiry option, so follow up with an expiry + if ($this->_Redis->setnx($key, $value)) { + return $this->_Redis->setTimeout($key, $duration); + } + + return false; + } + + /** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + */ + public function groups() + { + $result = []; + foreach ($this->_config['groups'] as $group) { + $value = $this->_Redis->get($this->_config['prefix'] . $group); + if (!$value) { + $value = 1; + $this->_Redis->set($this->_config['prefix'] . $group, $value); + } + $result[] = $group . $value; + } + + return $result; + } + + /** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @param string $group name of the group to be cleared + * @return bool success + */ + public function clearGroup($group) + { + return (bool)$this->_Redis->incr($this->_config['prefix'] . $group); + } + + /** + * Disconnects from the redis server + */ + public function __destruct() + { + if (empty($this->_config['persistent']) && $this->_Redis instanceof Redis) { + $this->_Redis->close(); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Cache/Engine/WincacheEngine.php b/app/vendor/cakephp/cakephp/src/Cache/Engine/WincacheEngine.php new file mode 100644 index 000000000..de3206754 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Cache/Engine/WincacheEngine.php @@ -0,0 +1,198 @@ +_key($key); + $duration = $this->_config['duration']; + + return wincache_ucache_set($key, $value, $duration); + } + + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, + * has expired, or if there was an error fetching it + */ + public function read($key) + { + $key = $this->_key($key); + + return wincache_ucache_get($key); + } + + /** + * Increments the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to increment + * @return bool|int New incremented value, false otherwise + */ + public function increment($key, $offset = 1) + { + $key = $this->_key($key); + + return wincache_ucache_inc($key, $offset); + } + + /** + * Decrements the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return bool|int New decremented value, false otherwise + */ + public function decrement($key, $offset = 1) + { + $key = $this->_key($key); + + return wincache_ucache_dec($key, $offset); + } + + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) + { + $key = $this->_key($key); + + return wincache_ucache_delete($key); + } + + /** + * Delete all keys from the cache. This will clear every + * item in the cache matching the cache config prefix. + * + * @param bool $check If true, nothing will be cleared, as entries will + * naturally expire in wincache.. + * @return bool True Returns true. + */ + public function clear($check) + { + if ($check) { + return true; + } + $info = wincache_ucache_info(); + $cacheKeys = $info['ucache_entries']; + unset($info); + foreach ($cacheKeys as $key) { + if (strpos($key['key_name'], $this->_config['prefix']) === 0) { + wincache_ucache_delete($key['key_name']); + } + } + + return true; + } + + /** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + */ + public function groups() + { + if (empty($this->_compiledGroupNames)) { + foreach ($this->_config['groups'] as $group) { + $this->_compiledGroupNames[] = $this->_config['prefix'] . $group; + } + } + + $groups = wincache_ucache_get($this->_compiledGroupNames); + if (count($groups) !== count($this->_config['groups'])) { + foreach ($this->_compiledGroupNames as $group) { + if (!isset($groups[$group])) { + wincache_ucache_set($group, 1); + $groups[$group] = 1; + } + } + ksort($groups); + } + + $result = []; + $groups = array_values($groups); + foreach ($this->_config['groups'] as $i => $group) { + $result[] = $group . $groups[$i]; + } + + return $result; + } + + /** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @param string $group The group to clear. + * @return bool success + */ + public function clearGroup($group) + { + $success = false; + wincache_ucache_inc($this->_config['prefix'] . $group, 1, $success); + + return $success; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Cache/Engine/XcacheEngine.php b/app/vendor/cakephp/cakephp/src/Cache/Engine/XcacheEngine.php new file mode 100644 index 000000000..9e1097f31 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Cache/Engine/XcacheEngine.php @@ -0,0 +1,256 @@ + 3600, + 'groups' => [], + 'prefix' => null, + 'probability' => 100, + 'PHP_AUTH_USER' => 'user', + 'PHP_AUTH_PW' => 'password' + ]; + + /** + * Initialize the Cache Engine + * + * Called automatically by the cache frontend + * + * @param array $config array of setting for the engine + * @return bool True if the engine has been successfully initialized, false if not + */ + public function init(array $config = []) + { + if (!extension_loaded('xcache')) { + return false; + } + + parent::init($config); + + return true; + } + + /** + * Write data for key into cache + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @return bool True if the data was successfully cached, false on failure + */ + public function write($key, $value) + { + $key = $this->_key($key); + + if (!is_numeric($value)) { + $value = serialize($value); + } + + $duration = $this->_config['duration']; + $expires = time() + $duration; + xcache_set($key . '_expires', $expires, $duration); + + return xcache_set($key, $value, $duration); + } + + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, + * has expired, or if there was an error fetching it + */ + public function read($key) + { + $key = $this->_key($key); + + if (xcache_isset($key)) { + $time = time(); + $cachetime = (int)xcache_get($key . '_expires'); + if ($cachetime < $time || ($time + $this->_config['duration']) < $cachetime) { + return false; + } + + $value = xcache_get($key); + if (is_string($value) && !is_numeric($value)) { + $value = unserialize($value); + } + + return $value; + } + + return false; + } + + /** + * Increments the value of an integer cached key + * If the cache key is not an integer it will be treated as 0 + * + * @param string $key Identifier for the data + * @param int $offset How much to increment + * @return bool|int New incremented value, false otherwise + */ + public function increment($key, $offset = 1) + { + $key = $this->_key($key); + + return xcache_inc($key, $offset); + } + + /** + * Decrements the value of an integer cached key. + * If the cache key is not an integer it will be treated as 0 + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return bool|int New decremented value, false otherwise + */ + public function decrement($key, $offset = 1) + { + $key = $this->_key($key); + + return xcache_dec($key, $offset); + } + + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) + { + $key = $this->_key($key); + + return xcache_unset($key); + } + + /** + * Delete all keys from the cache + * + * @param bool $check If true no deletes will occur and instead CakePHP will rely + * on key TTL values. + * Unused for Xcache engine. + * @return bool True if the cache was successfully cleared, false otherwise + */ + public function clear($check) + { + $this->_auth(); + $max = xcache_count(XC_TYPE_VAR); + for ($i = 0; $i < $max; $i++) { + xcache_clear_cache(XC_TYPE_VAR, $i); + } + $this->_auth(true); + + return true; + } + + /** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + */ + public function groups() + { + $result = []; + foreach ($this->_config['groups'] as $group) { + $value = xcache_get($this->_config['prefix'] . $group); + if (!$value) { + $value = 1; + xcache_set($this->_config['prefix'] . $group, $value, 0); + } + $result[] = $group . $value; + } + + return $result; + } + + /** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @param string $group The group to clear. + * @return bool success + */ + public function clearGroup($group) + { + return (bool)xcache_inc($this->_config['prefix'] . $group, 1); + } + + /** + * Populates and reverses $_SERVER authentication values + * Makes necessary changes (and reverting them back) in $_SERVER + * + * This has to be done because xcache_clear_cache() needs to pass Basic Http Auth + * (see xcache.admin configuration config) + * + * @param bool $reverse Revert changes + * @return void + */ + protected function _auth($reverse = false) + { + static $backup = []; + $keys = ['PHP_AUTH_USER' => 'user', 'PHP_AUTH_PW' => 'password']; + foreach ($keys as $key => $value) { + if ($reverse) { + if (isset($backup[$key])) { + $_SERVER[$key] = $backup[$key]; + unset($backup[$key]); + } else { + unset($_SERVER[$key]); + } + } else { + $value = env($key); + if (!empty($value)) { + $backup[$key] = $value; + } + if (!empty($this->_config[$value])) { + $_SERVER[$key] = $this->_config[$value]; + } elseif (!empty($this->_config[$key])) { + $_SERVER[$key] = $this->_config[$key]; + } else { + $_SERVER[$key] = $value; + } + } + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Cache/README.md b/app/vendor/cakephp/cakephp/src/Cache/README.md new file mode 100644 index 000000000..befe20fe4 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Cache/README.md @@ -0,0 +1,57 @@ +# CakePHP Caching Library + +The Cache library provides a `Cache` service locator for interfacing with multiple caching backends using +a simple to use interface. + +The caching backends supported are: + +* Files +* APC +* Memcached +* Redis +* Wincache +* Xcache + +## Usage + +Caching engines need to be configured with the `Cache::config()` method. + +```php +use Cake\Cache\Cache; + +// Using a short name +Cache::config('default', [ + 'className' => 'File', + 'duration' => '+1 hours', + 'path' => sys_get_tmp_dir(), + 'prefix' => 'my_app_' +]); + +// Using a fully namespaced name. +Cache::config('long', [ + 'className' => 'Cake\Cache\Engine\ApcuEngine', + 'duration' => '+1 week', + 'prefix' => 'my_app_' +]); + +// Using a constructed object. +$object = new FileEngine($config); +Cache::config('other', $object); +``` + +You can now read a write from the cache: + +```php +$data = Cache::remember('my_cache_key', function () { + return Service::expensiveCall(); +}); +``` + +The code above will try to look for data stored in cache under the `my_cache_key`, if not found +the callback will be executed and the returned data will be cached for future calls. + +## Documentation + +Please make sure you check the [official documentation](https://book.cakephp.org/3.0/en/core-libraries/caching.html) + + diff --git a/app/vendor/cakephp/cakephp/src/Cache/composer.json b/app/vendor/cakephp/cakephp/src/Cache/composer.json new file mode 100644 index 000000000..23289aa19 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Cache/composer.json @@ -0,0 +1,33 @@ +{ + "name": "cakephp/cache", + "description": "Easy to use Caching library with support for multiple caching backends", + "type": "library", + "keywords": [ + "cakephp", + "caching", + "cache" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/cache/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/cache" + }, + "require": { + "php": ">=5.6.0", + "cakephp/core": "^3.6.0" + }, + "autoload": { + "psr-4": { + "Cake\\Cache\\": "." + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Collection.php b/app/vendor/cakephp/cakephp/src/Collection/Collection.php new file mode 100644 index 000000000..07aa11f7a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Collection.php @@ -0,0 +1,112 @@ +buffered()); + } + + /** + * Unserializes the passed string and rebuilds the Collection instance + * + * @param string $collection The serialized collection + * @return void + */ + public function unserialize($collection) + { + $this->__construct(unserialize($collection)); + } + + /** + * {@inheritDoc} + * + * @return int + */ + public function count() + { + $traversable = $this->optimizeUnwrap(); + + if (is_array($traversable)) { + return count($traversable); + } + + return iterator_count($traversable); + } + + /** + * {@inheritDoc} + * + * @return int + */ + public function countKeys() + { + return count($this->toArray()); + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + return [ + 'count' => $this->count(), + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/CollectionInterface.php b/app/vendor/cakephp/cakephp/src/Collection/CollectionInterface.php new file mode 100644 index 000000000..951e23c92 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/CollectionInterface.php @@ -0,0 +1,1101 @@ +each(function ($value, $key) { + * echo "Element $key: $value"; + * }); + * ``` + * + * @param callable $c callable function that will receive each of the elements + * in this collection + * @return \Cake\Collection\CollectionInterface + */ + public function each(callable $c); + + /** + * Looks through each value in the collection, and returns another collection with + * all the values that pass a truth test. Only the values for which the callback + * returns true will be present in the resulting collection. + * + * Each time the callback is executed it will receive the value of the element + * in the current iteration, the key of the element and this collection as + * arguments, in that order. + * + * ### Example: + * + * Filtering odd numbers in an array, at the end only the value 2 will + * be present in the resulting collection: + * + * ``` + * $collection = (new Collection([1, 2, 3]))->filter(function ($value, $key) { + * return $value % 2 === 0; + * }); + * ``` + * + * @param callable|null $c the method that will receive each of the elements and + * returns true whether or not they should be in the resulting collection. + * If left null, a callback that filters out falsey values will be used. + * @return \Cake\Collection\CollectionInterface + */ + public function filter(callable $c = null); + + /** + * Looks through each value in the collection, and returns another collection with + * all the values that do not pass a truth test. This is the opposite of `filter`. + * + * Each time the callback is executed it will receive the value of the element + * in the current iteration, the key of the element and this collection as + * arguments, in that order. + * + * ### Example: + * + * Filtering even numbers in an array, at the end only values 1 and 3 will + * be present in the resulting collection: + * + * ``` + * $collection = (new Collection([1, 2, 3]))->reject(function ($value, $key) { + * return $value % 2 === 0; + * }); + * ``` + * + * @param callable $c the method that will receive each of the elements and + * returns true whether or not they should be out of the resulting collection. + * @return \Cake\Collection\CollectionInterface + */ + public function reject(callable $c); + + /** + * Returns true if all values in this collection pass the truth test provided + * in the callback. + * + * Each time the callback is executed it will receive the value of the element + * in the current iteration and the key of the element as arguments, in that + * order. + * + * ### Example: + * + * ``` + * $overTwentyOne = (new Collection([24, 45, 60, 15]))->every(function ($value, $key) { + * return $value > 21; + * }); + * ``` + * + * Empty collections always return true because it is a vacuous truth. + * + * @param callable $c a callback function + * @return bool true if for all elements in this collection the provided + * callback returns true, false otherwise. + */ + public function every(callable $c); + + /** + * Returns true if any of the values in this collection pass the truth test + * provided in the callback. + * + * Each time the callback is executed it will receive the value of the element + * in the current iteration and the key of the element as arguments, in that + * order. + * + * ### Example: + * + * ``` + * $hasYoungPeople = (new Collection([24, 45, 15]))->every(function ($value, $key) { + * return $value < 21; + * }); + * ``` + * + * @param callable $c a callback function + * @return bool true if the provided callback returns true for any element in this + * collection, false otherwise + */ + public function some(callable $c); + + /** + * Returns true if $value is present in this collection. Comparisons are made + * both by value and type. + * + * @param mixed $value The value to check for + * @return bool true if $value is present in this collection + */ + public function contains($value); + + /** + * Returns another collection after modifying each of the values in this one using + * the provided callable. + * + * Each time the callback is executed it will receive the value of the element + * in the current iteration, the key of the element and this collection as + * arguments, in that order. + * + * ### Example: + * + * Getting a collection of booleans where true indicates if a person is female: + * + * ``` + * $collection = (new Collection($people))->map(function ($person, $key) { + * return $person->gender === 'female'; + * }); + * ``` + * + * @param callable $c the method that will receive each of the elements and + * returns the new value for the key that is being iterated + * @return \Cake\Collection\CollectionInterface + */ + public function map(callable $c); + + /** + * Folds the values in this collection to a single value, as the result of + * applying the callback function to all elements. $zero is the initial state + * of the reduction, and each successive step of it should be returned + * by the callback function. + * If $zero is omitted the first value of the collection will be used in its place + * and reduction will start from the second item. + * + * @param callable $c The callback function to be called + * @param mixed $zero The state of reduction + * @return mixed + */ + public function reduce(callable $c, $zero = null); + + /** + * Returns a new collection containing the column or property value found in each + * of the elements, as requested in the $matcher param. + * + * The matcher can be a string with a property name to extract or a dot separated + * path of properties that should be followed to get the last one in the path. + * + * If a column or property could not be found for a particular element in the + * collection, that position is filled with null. + * + * ### Example: + * + * Extract the user name for all comments in the array: + * + * ``` + * $items = [ + * ['comment' => ['body' => 'cool', 'user' => ['name' => 'Mark']], + * ['comment' => ['body' => 'very cool', 'user' => ['name' => 'Renan']] + * ]; + * $extracted = (new Collection($items))->extract('comment.user.name'); + * + * // Result will look like this when converted to array + * ['Mark', 'Renan'] + * ``` + * + * It is also possible to extract a flattened collection out of nested properties + * + * ``` + * $items = [ + * ['comment' => ['votes' => [['value' => 1], ['value' => 2], ['value' => 3]]], + * ['comment' => ['votes' => [['value' => 4]] + * ]; + * $extracted = (new Collection($items))->extract('comment.votes.{*}.value'); + * + * // Result will contain + * [1, 2, 3, 4] + * ``` + * + * @param string $matcher a dot separated string symbolizing the path to follow + * inside the hierarchy of each value so that the column can be extracted. + * @return \Cake\Collection\CollectionInterface + */ + public function extract($matcher); + + /** + * Returns the top element in this collection after being sorted by a property. + * Check the sortBy method for information on the callback and $type parameters + * + * ### Examples: + * + * ``` + * // For a collection of employees + * $max = $collection->max('age'); + * $max = $collection->max('user.salary'); + * $max = $collection->max(function ($e) { + * return $e->get('user')->get('salary'); + * }); + * + * // Display employee name + * echo $max->name; + * ``` + * + * @param callable|string $callback the callback or column name to use for sorting + * @param int $type the type of comparison to perform, either SORT_STRING + * SORT_NUMERIC or SORT_NATURAL + * @see \Cake\Collection\CollectionIterface::sortBy() + * @return mixed The value of the top element in the collection + */ + public function max($callback, $type = \SORT_NUMERIC); + + /** + * Returns the bottom element in this collection after being sorted by a property. + * Check the sortBy method for information on the callback and $type parameters + * + * ### Examples: + * + * ``` + * // For a collection of employees + * $min = $collection->min('age'); + * $min = $collection->min('user.salary'); + * $min = $collection->min(function ($e) { + * return $e->get('user')->get('salary'); + * }); + * + * // Display employee name + * echo $min->name; + * ``` + * + * @param callable|string $callback the callback or column name to use for sorting + * @param int $type the type of comparison to perform, either SORT_STRING + * SORT_NUMERIC or SORT_NATURAL + * @see \Cake\Collection\CollectionInterface::sortBy() + * @return mixed The value of the bottom element in the collection + */ + public function min($callback, $type = \SORT_NUMERIC); + + /** + * Returns the average of all the values extracted with $matcher + * or of this collection. + * + * ### Example: + * + * ``` + * $items = [ + * ['invoice' => ['total' => 100]], + * ['invoice' => ['total' => 200]] + * ]; + * + * $total = (new Collection($items))->avg('invoice.total'); + * + * // Total: 150 + * + * $total = (new Collection([1, 2, 3]))->avg(); + * // Total: 2 + * ``` + * + * @param string|callable|null $matcher The property name to sum or a function + * If no value is passed, an identity function will be used. + * that will return the value of the property to sum. + * @return float|int|null + */ + public function avg($matcher = null); + + /** + * Returns the median of all the values extracted with $matcher + * or of this collection. + * + * ### Example: + * + * ``` + * $items = [ + * ['invoice' => ['total' => 400]], + * ['invoice' => ['total' => 500]] + * ['invoice' => ['total' => 100]] + * ['invoice' => ['total' => 333]] + * ['invoice' => ['total' => 200]] + * ]; + * + * $total = (new Collection($items))->median('invoice.total'); + * + * // Total: 333 + * + * $total = (new Collection([1, 2, 3, 4]))->median(); + * // Total: 2.5 + * ``` + * + * @param string|callable|null $matcher The property name to sum or a function + * If no value is passed, an identity function will be used. + * that will return the value of the property to sum. + * @return float|int|null + */ + public function median($matcher = null); + + /** + * Returns a sorted iterator out of the elements in this collection, + * ranked in ascending order by the results of running each value through a + * callback. $callback can also be a string representing the column or property + * name. + * + * The callback will receive as its first argument each of the elements in $items, + * the value returned by the callback will be used as the value for sorting such + * element. Please note that the callback function could be called more than once + * per element. + * + * ### Example: + * + * ``` + * $items = $collection->sortBy(function ($user) { + * return $user->age; + * }); + * + * // alternatively + * $items = $collection->sortBy('age'); + * + * // or use a property path + * $items = $collection->sortBy('department.name'); + * + * // output all user name order by their age in descending order + * foreach ($items as $user) { + * echo $user->name; + * } + * ``` + * + * @param callable|string $callback the callback or column name to use for sorting + * @param int $dir either SORT_DESC or SORT_ASC + * @param int $type the type of comparison to perform, either SORT_STRING + * SORT_NUMERIC or SORT_NATURAL + * @return \Cake\Collection\CollectionInterface + */ + public function sortBy($callback, $dir = SORT_DESC, $type = \SORT_NUMERIC); + + /** + * Splits a collection into sets, grouped by the result of running each value + * through the callback. If $callback is a string instead of a callable, + * groups by the property named by $callback on each of the values. + * + * When $callback is a string it should be a property name to extract or + * a dot separated path of properties that should be followed to get the last + * one in the path. + * + * ### Example: + * + * ``` + * $items = [ + * ['id' => 1, 'name' => 'foo', 'parent_id' => 10], + * ['id' => 2, 'name' => 'bar', 'parent_id' => 11], + * ['id' => 3, 'name' => 'baz', 'parent_id' => 10], + * ]; + * + * $group = (new Collection($items))->groupBy('parent_id'); + * + * // Or + * $group = (new Collection($items))->groupBy(function ($e) { + * return $e['parent_id']; + * }); + * + * // Result will look like this when converted to array + * [ + * 10 => [ + * ['id' => 1, 'name' => 'foo', 'parent_id' => 10], + * ['id' => 3, 'name' => 'baz', 'parent_id' => 10], + * ], + * 11 => [ + * ['id' => 2, 'name' => 'bar', 'parent_id' => 11], + * ] + * ]; + * ``` + * + * @param callable|string $callback the callback or column name to use for grouping + * or a function returning the grouping key out of the provided element + * @return \Cake\Collection\CollectionInterface + */ + public function groupBy($callback); + + /** + * Given a list and a callback function that returns a key for each element + * in the list (or a property name), returns an object with an index of each item. + * Just like groupBy, but for when you know your keys are unique. + * + * When $callback is a string it should be a property name to extract or + * a dot separated path of properties that should be followed to get the last + * one in the path. + * + * ### Example: + * + * ``` + * $items = [ + * ['id' => 1, 'name' => 'foo'], + * ['id' => 2, 'name' => 'bar'], + * ['id' => 3, 'name' => 'baz'], + * ]; + * + * $indexed = (new Collection($items))->indexBy('id'); + * + * // Or + * $indexed = (new Collection($items))->indexBy(function ($e) { + * return $e['id']; + * }); + * + * // Result will look like this when converted to array + * [ + * 1 => ['id' => 1, 'name' => 'foo'], + * 3 => ['id' => 3, 'name' => 'baz'], + * 2 => ['id' => 2, 'name' => 'bar'], + * ]; + * ``` + * + * @param callable|string $callback the callback or column name to use for indexing + * or a function returning the indexing key out of the provided element + * @return \Cake\Collection\CollectionInterface + */ + public function indexBy($callback); + + /** + * Sorts a list into groups and returns a count for the number of elements + * in each group. Similar to groupBy, but instead of returning a list of values, + * returns a count for the number of values in that group. + * + * When $callback is a string it should be a property name to extract or + * a dot separated path of properties that should be followed to get the last + * one in the path. + * + * ### Example: + * + * ``` + * $items = [ + * ['id' => 1, 'name' => 'foo', 'parent_id' => 10], + * ['id' => 2, 'name' => 'bar', 'parent_id' => 11], + * ['id' => 3, 'name' => 'baz', 'parent_id' => 10], + * ]; + * + * $group = (new Collection($items))->countBy('parent_id'); + * + * // Or + * $group = (new Collection($items))->countBy(function ($e) { + * return $e['parent_id']; + * }); + * + * // Result will look like this when converted to array + * [ + * 10 => 2, + * 11 => 1 + * ]; + * ``` + * + * @param callable|string $callback the callback or column name to use for indexing + * or a function returning the indexing key out of the provided element + * @return \Cake\Collection\CollectionInterface + */ + public function countBy($callback); + + /** + * Returns the total sum of all the values extracted with $matcher + * or of this collection. + * + * ### Example: + * + * ``` + * $items = [ + * ['invoice' => ['total' => 100]], + * ['invoice' => ['total' => 200]] + * ]; + * + * $total = (new Collection($items))->sumOf('invoice.total'); + * + * // Total: 300 + * + * $total = (new Collection([1, 2, 3]))->sumOf(); + * // Total: 6 + * ``` + * + * @param string|callable|null $matcher The property name to sum or a function + * If no value is passed, an identity function will be used. + * that will return the value of the property to sum. + * @return float|int + */ + public function sumOf($matcher = null); + + /** + * Returns a new collection with the elements placed in a random order, + * this function does not preserve the original keys in the collection. + * + * @return \Cake\Collection\CollectionInterface + */ + public function shuffle(); + + /** + * Returns a new collection with maximum $size random elements + * from this collection + * + * @param int $size the maximum number of elements to randomly + * take from this collection + * @return \Cake\Collection\CollectionInterface + */ + public function sample($size = 10); + + /** + * Returns a new collection with maximum $size elements in the internal + * order this collection was created. If a second parameter is passed, it + * will determine from what position to start taking elements. + * + * @param int $size the maximum number of elements to take from + * this collection + * @param int $from A positional offset from where to take the elements + * @return \Cake\Collection\CollectionInterface + */ + public function take($size = 1, $from = 0); + + /** + * Returns a new collection that will skip the specified amount of elements + * at the beginning of the iteration. + * + * @param int $howMany The number of elements to skip. + * @return \Cake\Collection\CollectionInterface + */ + public function skip($howMany); + + /** + * Looks through each value in the list, returning a Collection of all the + * values that contain all of the key-value pairs listed in $conditions. + * + * ### Example: + * + * ``` + * $items = [ + * ['comment' => ['body' => 'cool', 'user' => ['name' => 'Mark']], + * ['comment' => ['body' => 'very cool', 'user' => ['name' => 'Renan']] + * ]; + * + * $extracted = (new Collection($items))->match(['user.name' => 'Renan']); + * + * // Result will look like this when converted to array + * [ + * ['comment' => ['body' => 'very cool', 'user' => ['name' => 'Renan']] + * ] + * ``` + * + * @param array $conditions a key-value list of conditions where + * the key is a property path as accepted by `Collection::extract, + * and the value the condition against with each element will be matched + * @return \Cake\Collection\CollectionInterface + */ + public function match(array $conditions); + + /** + * Returns the first result matching all of the key-value pairs listed in + * conditions. + * + * @param array $conditions a key-value list of conditions where the key is + * a property path as accepted by `Collection::extract`, and the value the + * condition against with each element will be matched + * @see \Cake\Collection\CollectionInterface::match() + * @return mixed + */ + public function firstMatch(array $conditions); + + /** + * Returns the first result in this collection + * + * @return mixed The first value in the collection will be returned. + */ + public function first(); + + /** + * Returns the last result in this collection + * + * @return mixed The last value in the collection will be returned. + */ + public function last(); + + /** + * Returns a new collection as the result of concatenating the list of elements + * in this collection with the passed list of elements + * + * @param array|\Traversable $items Items list. + * @return \Cake\Collection\CollectionInterface + */ + public function append($items); + + /** + * Returns a new collection where the values extracted based on a value path + * and then indexed by a key path. Optionally this method can produce parent + * groups based on a group property path. + * + * ### Examples: + * + * ``` + * $items = [ + * ['id' => 1, 'name' => 'foo', 'parent' => 'a'], + * ['id' => 2, 'name' => 'bar', 'parent' => 'b'], + * ['id' => 3, 'name' => 'baz', 'parent' => 'a'], + * ]; + * + * $combined = (new Collection($items))->combine('id', 'name'); + * + * // Result will look like this when converted to array + * [ + * 1 => 'foo', + * 2 => 'bar', + * 3 => 'baz', + * ]; + * + * $combined = (new Collection($items))->combine('id', 'name', 'parent'); + * + * // Result will look like this when converted to array + * [ + * 'a' => [1 => 'foo', 3 => 'baz'], + * 'b' => [2 => 'bar'] + * ]; + * ``` + * + * @param callable|string $keyPath the column name path to use for indexing + * or a function returning the indexing key out of the provided element + * @param callable|string $valuePath the column name path to use as the array value + * or a function returning the value out of the provided element + * @param callable|string|null $groupPath the column name path to use as the parent + * grouping key or a function returning the key out of the provided element + * @return \Cake\Collection\CollectionInterface + */ + public function combine($keyPath, $valuePath, $groupPath = null); + + /** + * Returns a new collection where the values are nested in a tree-like structure + * based on an id property path and a parent id property path. + * + * @param callable|string $idPath the column name path to use for determining + * whether an element is parent of another + * @param callable|string $parentPath the column name path to use for determining + * whether an element is child of another + * @param string $nestingKey The key name under which children are nested + * @return \Cake\Collection\CollectionInterface + */ + public function nest($idPath, $parentPath, $nestingKey = 'children'); + + /** + * Returns a new collection containing each of the elements found in `$values` as + * a property inside the corresponding elements in this collection. The property + * where the values will be inserted is described by the `$path` parameter. + * + * The $path can be a string with a property name or a dot separated path of + * properties that should be followed to get the last one in the path. + * + * If a column or property could not be found for a particular element in the + * collection as part of the path, the element will be kept unchanged. + * + * ### Example: + * + * Insert ages into a collection containing users: + * + * ``` + * $items = [ + * ['comment' => ['body' => 'cool', 'user' => ['name' => 'Mark']], + * ['comment' => ['body' => 'awesome', 'user' => ['name' => 'Renan']] + * ]; + * $ages = [25, 28]; + * $inserted = (new Collection($items))->insert('comment.user.age', $ages); + * + * // Result will look like this when converted to array + * [ + * ['comment' => ['body' => 'cool', 'user' => ['name' => 'Mark', 'age' => 25]], + * ['comment' => ['body' => 'awesome', 'user' => ['name' => 'Renan', 'age' => 28]] + * ]; + * ``` + * + * @param string $path a dot separated string symbolizing the path to follow + * inside the hierarchy of each value so that the value can be inserted + * @param mixed $values The values to be inserted at the specified path, + * values are matched with the elements in this collection by its positional index. + * @return \Cake\Collection\CollectionInterface + */ + public function insert($path, $values); + + /** + * Returns an array representation of the results + * + * @param bool $preserveKeys whether to use the keys returned by this + * collection as the array keys. Keep in mind that it is valid for iterators + * to return the same key for different elements, setting this value to false + * can help getting all items if keys are not important in the result. + * @return array + */ + public function toArray($preserveKeys = true); + + /** + * Returns an numerically-indexed array representation of the results. + * This is equivalent to calling `toArray(false)` + * + * @return array + */ + public function toList(); + + /** + * Convert a result set into JSON. + * + * Part of JsonSerializable interface. + * + * @return array The data to convert to JSON + */ + public function jsonSerialize(); + + /** + * Iterates once all elements in this collection and executes all stacked + * operations of them, finally it returns a new collection with the result. + * This is useful for converting non-rewindable internal iterators into + * a collection that can be rewound and used multiple times. + * + * A common use case is to re-use the same variable for calculating different + * data. In those cases it may be helpful and more performant to first compile + * a collection and then apply more operations to it. + * + * ### Example: + * + * ``` + * $collection->map($mapper)->sortBy('age')->extract('name'); + * $compiled = $collection->compile(); + * $isJohnHere = $compiled->some($johnMatcher); + * $allButJohn = $compiled->filter($johnMatcher); + * ``` + * + * In the above example, had the collection not been compiled before, the + * iterations for `map`, `sortBy` and `extract` would've been executed twice: + * once for getting `$isJohnHere` and once for `$allButJohn` + * + * You can think of this method as a way to create save points for complex + * calculations in a collection. + * + * @param bool $preserveKeys whether to use the keys returned by this + * collection as the array keys. Keep in mind that it is valid for iterators + * to return the same key for different elements, setting this value to false + * can help getting all items if keys are not important in the result. + * @return \Cake\Collection\CollectionInterface + */ + public function compile($preserveKeys = true); + + /** + * Returns a new collection where the operations performed by this collection. + * No matter how many times the new collection is iterated, those operations will + * only be performed once. + * + * This can also be used to make any non-rewindable iterator rewindable. + * + * @return \Cake\Collection\CollectionInterface + */ + public function buffered(); + + /** + * Returns a new collection with each of the elements of this collection + * after flattening the tree structure. The tree structure is defined + * by nesting elements under a key with a known name. It is possible + * to specify such name by using the '$nestingKey' parameter. + * + * By default all elements in the tree following a Depth First Search + * will be returned, that is, elements from the top parent to the leaves + * for each branch. + * + * It is possible to return all elements from bottom to top using a Breadth First + * Search approach by passing the '$dir' parameter with 'asc'. That is, it will + * return all elements for the same tree depth first and from bottom to top. + * + * Finally, you can specify to only get a collection with the leaf nodes in the + * tree structure. You do so by passing 'leaves' in the first argument. + * + * The possible values for the first argument are aliases for the following + * constants and it is valid to pass those instead of the alias: + * + * - desc: TreeIterator::SELF_FIRST + * - asc: TreeIterator::CHILD_FIRST + * - leaves: TreeIterator::LEAVES_ONLY + * + * ### Example: + * + * ``` + * $collection = new Collection([ + * ['id' => 1, 'children' => [['id' => 2, 'children' => [['id' => 3]]]]], + * ['id' => 4, 'children' => [['id' => 5]]] + * ]); + * $flattenedIds = $collection->listNested()->extract('id'); // Yields [1, 2, 3, 4, 5] + * ``` + * + * @param string|int $dir The direction in which to return the elements + * @param string|callable $nestingKey The key name under which children are nested + * or a callable function that will return the children list + * @return \Cake\Collection\CollectionInterface + */ + public function listNested($dir = 'desc', $nestingKey = 'children'); + + /** + * Creates a new collection that when iterated will stop yielding results if + * the provided condition evaluates to false. + * + * This is handy for dealing with infinite iterators or any generator that + * could start returning invalid elements at a certain point. For example, + * when reading lines from a file stream you may want to stop the iteration + * after a certain value is reached. + * + * ### Example: + * + * Get an array of lines in a CSV file until the timestamp column is less than a date + * + * ``` + * $lines = (new Collection($fileLines))->stopWhen(function ($value, $key) { + * return (new DateTime($value))->format('Y') < 2012; + * }) + * ->toArray(); + * ``` + * + * Get elements until the first unapproved message is found: + * + * ``` + * $comments = (new Collection($comments))->stopWhen(['is_approved' => false]); + * ``` + * + * @param callable $condition the method that will receive each of the elements and + * returns false when the iteration should be stopped. + * If an array, it will be interpreted as a key-value list of conditions where + * the key is a property path as accepted by `Collection::extract`, + * and the value the condition against with each element will be matched. + * @return \Cake\Collection\CollectionInterface + */ + public function stopWhen($condition); + + /** + * Creates a new collection where the items are the + * concatenation of the lists of items generated by the transformer function + * applied to each item in the original collection. + * + * The transformer function will receive the value and the key for each of the + * items in the collection, in that order, and it must return an array or a + * Traversable object that can be concatenated to the final result. + * + * If no transformer function is passed, an "identity" function will be used. + * This is useful when each of the elements in the source collection are + * lists of items to be appended one after another. + * + * ### Example: + * + * ``` + * $items [[1, 2, 3], [4, 5]]; + * $unfold = (new Collection($items))->unfold(); // Returns [1, 2, 3, 4, 5] + * ``` + * + * Using a transformer + * + * ``` + * $items [1, 2, 3]; + * $allItems = (new Collection($items))->unfold(function ($page) { + * return $service->fetchPage($page)->toArray(); + * }); + * ``` + * + * @param callable|null $transformer A callable function that will receive each of + * the items in the collection and should return an array or Traversable object + * @return \Cake\Collection\CollectionInterface + */ + public function unfold(callable $transformer = null); + + /** + * Passes this collection through a callable as its first argument. + * This is useful for decorating the full collection with another object. + * + * ### Example: + * + * ``` + * $items = [1, 2, 3]; + * $decorated = (new Collection($items))->through(function ($collection) { + * return new MyCustomCollection($collection); + * }); + * ``` + * + * @param callable $handler A callable function that will receive + * this collection as first argument. + * @return \Cake\Collection\CollectionInterface + */ + public function through(callable $handler); + + /** + * Combines the elements of this collection with each of the elements of the + * passed iterables, using their positional index as a reference. + * + * ### Example: + * + * ``` + * $collection = new Collection([1, 2]); + * $collection->zip([3, 4], [5, 6])->toList(); // returns [[1, 3, 5], [2, 4, 6]] + * ``` + * + * @param array|\Traversable ...$items The collections to zip. + * @return \Cake\Collection\CollectionInterface + */ + public function zip($items); + + /** + * Combines the elements of this collection with each of the elements of the + * passed iterables, using their positional index as a reference. + * + * The resulting element will be the return value of the $callable function. + * + * ### Example: + * + * ``` + * $collection = new Collection([1, 2]); + * $zipped = $collection->zipWith([3, 4], [5, 6], function (...$args) { + * return array_sum($args); + * }); + * $zipped->toList(); // returns [9, 12]; [(1 + 3 + 5), (2 + 4 + 6)] + * ``` + * + * @param array|\Traversable ...$items The collections to zip. + * @param callable $callable The function to use for zipping the elements together. + * @return \Cake\Collection\CollectionInterface + */ + public function zipWith($items, $callable); + + /** + * Breaks the collection into smaller arrays of the given size. + * + * ### Example: + * + * ``` + * $items [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + * $chunked = (new Collection($items))->chunk(3)->toList(); + * // Returns [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11]] + * ``` + * + * @param int $chunkSize The maximum size for each chunk + * @return \Cake\Collection\CollectionInterface + */ + public function chunk($chunkSize); + + /** + * Breaks the collection into smaller arrays of the given size. + * + * ### Example: + * + * ``` + * $items ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6]; + * $chunked = (new Collection($items))->chunkWithKeys(3)->toList(); + * // Returns [['a' => 1, 'b' => 2, 'c' => 3], ['d' => 4, 'e' => 5, 'f' => 6]] + * ``` + * + * @param int $chunkSize The maximum size for each chunk + * @param bool $preserveKeys If the keys of the array should be preserved + * @return \Cake\Collection\CollectionInterface + */ + public function chunkWithKeys($chunkSize, $preserveKeys = true); + + /** + * Returns whether or not there are elements in this collection + * + * ### Example: + * + * ``` + * $items [1, 2, 3]; + * (new Collection($items))->isEmpty(); // false + * ``` + * + * ``` + * (new Collection([]))->isEmpty(); // true + * ``` + * + * @return bool + */ + public function isEmpty(); + + /** + * Returns the closest nested iterator that can be safely traversed without + * losing any possible transformations. This is used mainly to remove empty + * IteratorIterator wrappers that can only slowdown the iteration process. + * + * @return \Traversable + */ + public function unwrap(); + + /** + * Transpose rows and columns into columns and rows + * + * ### Example: + * + * ``` + * $items = [ + * ['Products', '2012', '2013', '2014'], + * ['Product A', '200', '100', '50'], + * ['Product B', '300', '200', '100'], + * ['Product C', '400', '300', '200'], + * ] + * + * $transpose = (new Collection($items))->transpose()->toList(); + * + * // Returns + * // [ + * // ['Products', 'Product A', 'Product B', 'Product C'], + * // ['2012', '200', '300', '400'], + * // ['2013', '100', '200', '300'], + * // ['2014', '50', '100', '200'], + * // ] + * ``` + * + * @return \Cake\Collection\CollectionInterface + */ + public function transpose(); + + /** + * Returns the amount of elements in the collection. + * + * ## WARNINGS: + * + * ### Consumes all elements for NoRewindIterator collections: + * + * On certain type of collections, calling this method may render unusable afterwards. + * That is, you may not be able to get elements out of it, or to iterate on it anymore. + * + * Specifically any collection wrapping a Generator (a function with a yield statement) + * or a unbuffered database cursor will not accept any other function calls after calling + * `count()` on it. + * + * Create a new collection with `buffered()` method to overcome this problem. + * + * ### Can report more elements than unique keys: + * + * Any collection constructed by appending collections together, or by having internal iterators + * returning duplicate keys, will report a larger amount of elements using this functions than + * the final amount of elements when converting the collections to a keyed array. This is because + * duplicate keys will be collapsed into a single one in the final array, whereas this count method + * is only concerned by the amount of elements after converting it to a plain list. + * + * If you need the count of elements after taking the keys in consideration + * (the count of unique keys), you can call `countKeys()` + * + * ### Will change the current position of the iterator: + * + * Calling this method at the same time that you are iterating this collections, for example in + * a foreach, will result in undefined behavior. Avoid doing this. + * + * + * @return int + */ + public function count(); + + /** + * Returns the number of unique keys in this iterator. This is, the number of + * elements the collection will contain after calling `toArray()` + * + * This method comes with a number of caveats. Please refer to `CollectionInterface::count()` + * for details. + * + * @see \Cake\Collection\CollectionInterface::count() + * @return int + */ + public function countKeys(); +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/CollectionTrait.php b/app/vendor/cakephp/cakephp/src/Collection/CollectionTrait.php new file mode 100644 index 000000000..bb9fb6cf5 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/CollectionTrait.php @@ -0,0 +1,892 @@ +optimizeUnwrap() as $k => $v) { + $c($v, $k); + } + + return $this; + } + + /** + * {@inheritDoc} + * + * @return \Cake\Collection\Iterator\FilterIterator + */ + public function filter(callable $c = null) + { + if ($c === null) { + $c = function ($v) { + return (bool)$v; + }; + } + + return new FilterIterator($this->unwrap(), $c); + } + + /** + * {@inheritDoc} + * + * @return \Cake\Collection\Iterator\FilterIterator + */ + public function reject(callable $c) + { + return new FilterIterator($this->unwrap(), function ($key, $value, $items) use ($c) { + return !$c($key, $value, $items); + }); + } + + /** + * {@inheritDoc} + */ + public function every(callable $c) + { + foreach ($this->optimizeUnwrap() as $key => $value) { + if (!$c($value, $key)) { + return false; + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function some(callable $c) + { + foreach ($this->optimizeUnwrap() as $key => $value) { + if ($c($value, $key) === true) { + return true; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function contains($value) + { + foreach ($this->optimizeUnwrap() as $v) { + if ($value === $v) { + return true; + } + } + + return false; + } + + /** + * {@inheritDoc} + * + * @return \Cake\Collection\Iterator\ReplaceIterator + */ + public function map(callable $c) + { + return new ReplaceIterator($this->unwrap(), $c); + } + + /** + * {@inheritDoc} + */ + public function reduce(callable $c, $zero = null) + { + $isFirst = false; + if (func_num_args() < 2) { + $isFirst = true; + } + + $result = $zero; + foreach ($this->optimizeUnwrap() as $k => $value) { + if ($isFirst) { + $result = $value; + $isFirst = false; + continue; + } + $result = $c($result, $value, $k); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function extract($matcher) + { + $extractor = new ExtractIterator($this->unwrap(), $matcher); + if (is_string($matcher) && strpos($matcher, '{*}') !== false) { + $extractor = $extractor + ->filter(function ($data) { + return $data !== null && ($data instanceof Traversable || is_array($data)); + }) + ->unfold(); + } + + return $extractor; + } + + /** + * {@inheritDoc} + */ + public function max($callback, $type = \SORT_NUMERIC) + { + return (new SortIterator($this->unwrap(), $callback, \SORT_DESC, $type))->first(); + } + + /** + * {@inheritDoc} + */ + public function min($callback, $type = \SORT_NUMERIC) + { + return (new SortIterator($this->unwrap(), $callback, \SORT_ASC, $type))->first(); + } + + /** + * {@inheritDoc} + */ + public function avg($matcher = null) + { + $result = $this; + if ($matcher != null) { + $result = $result->extract($matcher); + } + $result = $result + ->reduce(function ($acc, $current) { + list($count, $sum) = $acc; + + return [$count + 1, $sum + $current]; + }, [0, 0]); + + if ($result[0] === 0) { + return null; + } + + return $result[1] / $result[0]; + } + + /** + * {@inheritDoc} + */ + public function median($matcher = null) + { + $elements = $this; + if ($matcher != null) { + $elements = $elements->extract($matcher); + } + $values = $elements->toList(); + sort($values); + $count = count($values); + + if ($count === 0) { + return null; + } + + $middle = (int)($count / 2); + + if ($count % 2) { + return $values[$middle]; + } + + return ($values[$middle - 1] + $values[$middle]) / 2; + } + + /** + * {@inheritDoc} + */ + public function sortBy($callback, $dir = \SORT_DESC, $type = \SORT_NUMERIC) + { + return new SortIterator($this->unwrap(), $callback, $dir, $type); + } + + /** + * {@inheritDoc} + */ + public function groupBy($callback) + { + $callback = $this->_propertyExtractor($callback); + $group = []; + foreach ($this->optimizeUnwrap() as $value) { + $group[$callback($value)][] = $value; + } + + return new Collection($group); + } + + /** + * {@inheritDoc} + */ + public function indexBy($callback) + { + $callback = $this->_propertyExtractor($callback); + $group = []; + foreach ($this->optimizeUnwrap() as $value) { + $group[$callback($value)] = $value; + } + + return new Collection($group); + } + + /** + * {@inheritDoc} + */ + public function countBy($callback) + { + $callback = $this->_propertyExtractor($callback); + + $mapper = function ($value, $key, $mr) use ($callback) { + $mr->emitIntermediate($value, $callback($value)); + }; + + $reducer = function ($values, $key, $mr) { + $mr->emit(count($values), $key); + }; + + return new Collection(new MapReduce($this->unwrap(), $mapper, $reducer)); + } + + /** + * {@inheritDoc} + */ + public function sumOf($matcher = null) + { + if ($matcher === null) { + return array_sum($this->toList()); + } + + $callback = $this->_propertyExtractor($matcher); + $sum = 0; + foreach ($this->optimizeUnwrap() as $k => $v) { + $sum += $callback($v, $k); + } + + return $sum; + } + + /** + * {@inheritDoc} + */ + public function shuffle() + { + $elements = $this->toArray(); + shuffle($elements); + + return new Collection($elements); + } + + /** + * {@inheritDoc} + */ + public function sample($size = 10) + { + return new Collection(new LimitIterator($this->shuffle(), 0, $size)); + } + + /** + * {@inheritDoc} + */ + public function take($size = 1, $from = 0) + { + return new Collection(new LimitIterator($this, $from, $size)); + } + + /** + * {@inheritDoc} + */ + public function skip($howMany) + { + return new Collection(new LimitIterator($this, $howMany)); + } + + /** + * {@inheritDoc} + */ + public function match(array $conditions) + { + return $this->filter($this->_createMatcherFilter($conditions)); + } + + /** + * {@inheritDoc} + */ + public function firstMatch(array $conditions) + { + return $this->match($conditions)->first(); + } + + /** + * {@inheritDoc} + */ + public function first() + { + $iterator = new LimitIterator($this, 0, 1); + foreach ($iterator as $result) { + return $result; + } + } + + /** + * {@inheritDoc} + */ + public function last() + { + $iterator = $this->optimizeUnwrap(); + if (is_array($iterator)) { + return array_pop($iterator); + } + + if ($iterator instanceof Countable) { + $count = count($iterator); + if ($count === 0) { + return null; + } + $iterator = new LimitIterator($iterator, $count - 1, 1); + } + + $result = null; + foreach ($iterator as $result) { + // No-op + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function append($items) + { + $list = new AppendIterator(); + $list->append($this->unwrap()); + $list->append((new Collection($items))->unwrap()); + + return new Collection($list); + } + + /** + * {@inheritDoc} + */ + public function appendItem($item, $key = null) + { + if ($key !== null) { + $data = [$key => $item]; + } else { + $data = [$item]; + } + + return $this->append($data); + } + + /** + * {@inheritDoc} + */ + public function prepend($items) + { + return (new Collection($items))->append($this); + } + + /** + * {@inheritDoc} + */ + public function prependItem($item, $key = null) + { + if ($key !== null) { + $data = [$key => $item]; + } else { + $data = [$item]; + } + + return $this->prepend($data); + } + + /** + * {@inheritDoc} + */ + public function combine($keyPath, $valuePath, $groupPath = null) + { + $options = [ + 'keyPath' => $this->_propertyExtractor($keyPath), + 'valuePath' => $this->_propertyExtractor($valuePath), + 'groupPath' => $groupPath ? $this->_propertyExtractor($groupPath) : null + ]; + + $mapper = function ($value, $key, $mapReduce) use ($options) { + $rowKey = $options['keyPath']; + $rowVal = $options['valuePath']; + + if (!$options['groupPath']) { + $mapReduce->emit($rowVal($value, $key), $rowKey($value, $key)); + + return null; + } + + $key = $options['groupPath']($value, $key); + $mapReduce->emitIntermediate( + [$rowKey($value, $key) => $rowVal($value, $key)], + $key + ); + }; + + $reducer = function ($values, $key, $mapReduce) { + $result = []; + foreach ($values as $value) { + $result += $value; + } + $mapReduce->emit($result, $key); + }; + + return new Collection(new MapReduce($this->unwrap(), $mapper, $reducer)); + } + + /** + * {@inheritDoc} + */ + public function nest($idPath, $parentPath, $nestingKey = 'children') + { + $parents = []; + $idPath = $this->_propertyExtractor($idPath); + $parentPath = $this->_propertyExtractor($parentPath); + $isObject = true; + + $mapper = function ($row, $key, $mapReduce) use (&$parents, $idPath, $parentPath, $nestingKey) { + $row[$nestingKey] = []; + $id = $idPath($row, $key); + $parentId = $parentPath($row, $key); + $parents[$id] =& $row; + $mapReduce->emitIntermediate($id, $parentId); + }; + + $reducer = function ($values, $key, $mapReduce) use (&$parents, &$isObject, $nestingKey) { + static $foundOutType = false; + if (!$foundOutType) { + $isObject = is_object(current($parents)); + $foundOutType = true; + } + if (empty($key) || !isset($parents[$key])) { + foreach ($values as $id) { + $parents[$id] = $isObject ? $parents[$id] : new ArrayIterator($parents[$id], 1); + $mapReduce->emit($parents[$id]); + } + + return null; + } + + $children = []; + foreach ($values as $id) { + $children[] =& $parents[$id]; + } + $parents[$key][$nestingKey] = $children; + }; + + return (new Collection(new MapReduce($this->unwrap(), $mapper, $reducer))) + ->map(function ($value) use (&$isObject) { + return $isObject ? $value : $value->getArrayCopy(); + }); + } + + /** + * {@inheritDoc} + * + * @return \Cake\Collection\Iterator\InsertIterator + */ + public function insert($path, $values) + { + return new InsertIterator($this->unwrap(), $path, $values); + } + + /** + * {@inheritDoc} + */ + public function toArray($preserveKeys = true) + { + $iterator = $this->unwrap(); + if ($iterator instanceof ArrayIterator) { + $items = $iterator->getArrayCopy(); + + return $preserveKeys ? $items : array_values($items); + } + // RecursiveIteratorIterator can return duplicate key values causing + // data loss when converted into an array + if ($preserveKeys && get_class($iterator) === 'RecursiveIteratorIterator') { + $preserveKeys = false; + } + + return iterator_to_array($this, $preserveKeys); + } + + /** + * {@inheritDoc} + */ + public function toList() + { + return $this->toArray(false); + } + + /** + * {@inheritDoc} + */ + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * {@inheritDoc} + */ + public function compile($preserveKeys = true) + { + return new Collection($this->toArray($preserveKeys)); + } + + /** + * {@inheritDoc} + * + * @return \Cake\Collection\Iterator\BufferedIterator + */ + public function buffered() + { + return new BufferedIterator($this->unwrap()); + } + + /** + * {@inheritDoc} + * + * @return \Cake\Collection\Iterator\TreeIterator + */ + public function listNested($dir = 'desc', $nestingKey = 'children') + { + $dir = strtolower($dir); + $modes = [ + 'desc' => TreeIterator::SELF_FIRST, + 'asc' => TreeIterator::CHILD_FIRST, + 'leaves' => TreeIterator::LEAVES_ONLY + ]; + + return new TreeIterator( + new NestIterator($this, $nestingKey), + isset($modes[$dir]) ? $modes[$dir] : $dir + ); + } + + /** + * {@inheritDoc} + * + * @return \Cake\Collection\Iterator\StoppableIterator + */ + public function stopWhen($condition) + { + if (!is_callable($condition)) { + $condition = $this->_createMatcherFilter($condition); + } + + return new StoppableIterator($this->unwrap(), $condition); + } + + /** + * {@inheritDoc} + */ + public function unfold(callable $transformer = null) + { + if ($transformer === null) { + $transformer = function ($item) { + return $item; + }; + } + + return new Collection( + new RecursiveIteratorIterator( + new UnfoldIterator($this->unwrap(), $transformer), + RecursiveIteratorIterator::LEAVES_ONLY + ) + ); + } + + /** + * {@inheritDoc} + */ + public function through(callable $handler) + { + $result = $handler($this); + + return $result instanceof CollectionInterface ? $result : new Collection($result); + } + + /** + * {@inheritDoc} + */ + public function zip($items) + { + return new ZipIterator(array_merge([$this->unwrap()], func_get_args())); + } + + /** + * {@inheritDoc} + */ + public function zipWith($items, $callable) + { + if (func_num_args() > 2) { + $items = func_get_args(); + $callable = array_pop($items); + } else { + $items = [$items]; + } + + return new ZipIterator(array_merge([$this->unwrap()], $items), $callable); + } + + /** + * {@inheritDoc} + */ + public function chunk($chunkSize) + { + return $this->map(function ($v, $k, $iterator) use ($chunkSize) { + $values = [$v]; + for ($i = 1; $i < $chunkSize; $i++) { + $iterator->next(); + if (!$iterator->valid()) { + break; + } + $values[] = $iterator->current(); + } + + return $values; + }); + } + + /** + * {@inheritDoc} + */ + public function chunkWithKeys($chunkSize, $preserveKeys = true) + { + return $this->map(function ($v, $k, $iterator) use ($chunkSize, $preserveKeys) { + $key = 0; + if ($preserveKeys) { + $key = $k; + } + $values = [$key => $v]; + for ($i = 1; $i < $chunkSize; $i++) { + $iterator->next(); + if (!$iterator->valid()) { + break; + } + if ($preserveKeys) { + $values[$iterator->key()] = $iterator->current(); + } else { + $values[] = $iterator->current(); + } + } + + return $values; + }); + } + + /** + * {@inheritDoc} + */ + public function isEmpty() + { + foreach ($this as $el) { + return false; + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function unwrap() + { + $iterator = $this; + while (get_class($iterator) === 'Cake\Collection\Collection') { + $iterator = $iterator->getInnerIterator(); + } + + if ($iterator !== $this && $iterator instanceof CollectionInterface) { + $iterator = $iterator->unwrap(); + } + + return $iterator; + } + + /** + * Backwards compatible wrapper for unwrap() + * + * @return \Traversable + * @deprecated 3.0.10 Will be removed in 4.0.0 + */ + // @codingStandardsIgnoreLine + public function _unwrap() + { + deprecationWarning('CollectionTrait::_unwrap() is deprecated. Use CollectionTrait::unwrap() instead.'); + + return $this->unwrap(); + } + + /** + * {@inheritDoc} + * + * @return \Cake\Collection\CollectionInterface + */ + public function cartesianProduct(callable $operation = null, callable $filter = null) + { + if ($this->isEmpty()) { + return new Collection([]); + } + + $collectionArrays = []; + $collectionArraysKeys = []; + $collectionArraysCounts = []; + + foreach ($this->toList() as $value) { + $valueCount = count($value); + if ($valueCount !== count($value, COUNT_RECURSIVE)) { + throw new LogicException('Cannot find the cartesian product of a multidimensional array'); + } + + $collectionArraysKeys[] = array_keys($value); + $collectionArraysCounts[] = $valueCount; + $collectionArrays[] = $value; + } + + $result = []; + $lastIndex = count($collectionArrays) - 1; + // holds the indexes of the arrays that generate the current combination + $currentIndexes = array_fill(0, $lastIndex + 1, 0); + + $changeIndex = $lastIndex; + + while (!($changeIndex === 0 && $currentIndexes[0] === $collectionArraysCounts[0])) { + $currentCombination = array_map(function ($value, $keys, $index) { + return $value[$keys[$index]]; + }, $collectionArrays, $collectionArraysKeys, $currentIndexes); + + if ($filter === null || $filter($currentCombination)) { + $result[] = ($operation === null) ? $currentCombination : $operation($currentCombination); + } + + $currentIndexes[$lastIndex]++; + + for ($changeIndex = $lastIndex; $currentIndexes[$changeIndex] === $collectionArraysCounts[$changeIndex] && $changeIndex > 0; $changeIndex--) { + $currentIndexes[$changeIndex] = 0; + $currentIndexes[$changeIndex - 1]++; + } + } + + return new Collection($result); + } + + /** + * {@inheritDoc} + * + * @return \Cake\Collection\CollectionInterface + */ + public function transpose() + { + $arrayValue = $this->toList(); + $length = count(current($arrayValue)); + $result = []; + foreach ($arrayValue as $column => $row) { + if (count($row) != $length) { + throw new LogicException('Child arrays do not have even length'); + } + } + + for ($column = 0; $column < $length; $column++) { + $result[] = array_column($arrayValue, $column); + } + + return new Collection($result); + } + + /** + * {@inheritDoc} + * + * @return int + */ + public function count() + { + $traversable = $this->optimizeUnwrap(); + + if (is_array($traversable)) { + return count($traversable); + } + + return iterator_count($traversable); + } + + /** + * {@inheritDoc} + * + * @return int + */ + public function countKeys() + { + return count($this->toArray()); + } + + /** + * Unwraps this iterator and returns the simplest + * traversable that can be used for getting the data out + * + * @return \Traversable|array + */ + protected function optimizeUnwrap() + { + $iterator = $this->unwrap(); + + if (get_class($iterator) === ArrayIterator::class) { + $iterator = $iterator->getArrayCopy(); + } + + return $iterator; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/ExtractTrait.php b/app/vendor/cakephp/cakephp/src/Collection/ExtractTrait.php new file mode 100644 index 000000000..2beb22dd3 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/ExtractTrait.php @@ -0,0 +1,147 @@ +_extract($element, $path); + }; + } + + return function ($element) use ($path) { + return $this->_simpleExtract($element, $path); + }; + } + + /** + * Returns a column from $data that can be extracted + * by iterating over the column names contained in $path. + * It will return arrays for elements in represented with `{*}` + * + * @param array|\ArrayAccess $data Data. + * @param array $path Path to extract from. + * @return mixed + */ + protected function _extract($data, $path) + { + $value = null; + $collectionTransform = false; + + foreach ($path as $i => $column) { + if ($column === '{*}') { + $collectionTransform = true; + continue; + } + + if ($collectionTransform && + !($data instanceof Traversable || is_array($data))) { + return null; + } + + if ($collectionTransform) { + $rest = implode('.', array_slice($path, $i)); + + return (new Collection($data))->extract($rest); + } + + if (!isset($data[$column])) { + return null; + } + + $value = $data[$column]; + $data = $value; + } + + return $value; + } + + /** + * Returns a column from $data that can be extracted + * by iterating over the column names contained in $path + * + * @param array|\ArrayAccess $data Data. + * @param array $path Path to extract from. + * @return mixed + */ + protected function _simpleExtract($data, $path) + { + $value = null; + foreach ($path as $column) { + if (!isset($data[$column])) { + return null; + } + $value = $data[$column]; + $data = $value; + } + + return $value; + } + + /** + * Returns a callable that receives a value and will return whether or not + * it matches certain condition. + * + * @param array $conditions A key-value list of conditions to match where the + * key is the property path to get from the current item and the value is the + * value to be compared the item with. + * @return callable + */ + protected function _createMatcherFilter(array $conditions) + { + $matchers = []; + foreach ($conditions as $property => $value) { + $extractor = $this->_propertyExtractor($property); + $matchers[] = function ($v) use ($extractor, $value) { + return $extractor($v) == $value; + }; + } + + return function ($value) use ($matchers) { + foreach ($matchers as $match) { + if (!$match($value)) { + return false; + } + } + + return true; + }; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/BufferedIterator.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/BufferedIterator.php new file mode 100644 index 000000000..cc7adf0c5 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/BufferedIterator.php @@ -0,0 +1,212 @@ +_buffer = new SplDoublyLinkedList(); + parent::__construct($items); + } + + /** + * Returns the current key in the iterator + * + * @return mixed + */ + public function key() + { + return $this->_key; + } + + /** + * Returns the current record in the iterator + * + * @return mixed + */ + public function current() + { + return $this->_current; + } + + /** + * Rewinds the collection + * + * @return void + */ + public function rewind() + { + if ($this->_index === 0 && !$this->_started) { + $this->_started = true; + parent::rewind(); + + return; + } + + $this->_index = 0; + } + + /** + * Returns whether or not the iterator has more elements + * + * @return bool + */ + public function valid() + { + if ($this->_buffer->offsetExists($this->_index)) { + $current = $this->_buffer->offsetGet($this->_index); + $this->_current = $current['value']; + $this->_key = $current['key']; + + return true; + } + + $valid = parent::valid(); + + if ($valid) { + $this->_current = parent::current(); + $this->_key = parent::key(); + $this->_buffer->push([ + 'key' => $this->_key, + 'value' => $this->_current + ]); + } + + $this->_finished = !$valid; + + return $valid; + } + + /** + * Advances the iterator pointer to the next element + * + * @return void + */ + public function next() + { + $this->_index++; + + if (!$this->_finished) { + parent::next(); + } + } + + /** + * Returns the number or items in this collection + * + * @return int + */ + public function count() + { + if (!$this->_started) { + $this->rewind(); + } + + while ($this->valid()) { + $this->next(); + } + + return $this->_buffer->count(); + } + + /** + * Returns a string representation of this object that can be used + * to reconstruct it + * + * @return string + */ + public function serialize() + { + if (!$this->_finished) { + $this->count(); + } + + return serialize($this->_buffer); + } + + /** + * Unserializes the passed string and rebuilds the BufferedIterator instance + * + * @param string $buffer The serialized buffer iterator + * @return void + */ + public function unserialize($buffer) + { + $this->__construct([]); + $this->_buffer = unserialize($buffer); + $this->_started = true; + $this->_finished = true; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/ExtractIterator.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/ExtractIterator.php new file mode 100644 index 000000000..2ffe139b7 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/ExtractIterator.php @@ -0,0 +1,107 @@ + ['body' => 'cool', 'user' => ['name' => 'Mark']], + * ['comment' => ['body' => 'very cool', 'user' => ['name' => 'Renan']] + * ]; + * $extractor = new ExtractIterator($items, 'comment.user.name''); + * ``` + * + * @param array|\Traversable $items The list of values to iterate + * @param string $path a dot separated string symbolizing the path to follow + * inside the hierarchy of each value so that the column can be extracted. + */ + public function __construct($items, $path) + { + $this->_extractor = $this->_propertyExtractor($path); + parent::__construct($items); + } + + /** + * Returns the column value defined in $path or null if the path could not be + * followed + * + * @return mixed + */ + public function current() + { + $extractor = $this->_extractor; + + return $extractor(parent::current()); + } + + /** + * {@inheritDoc} + * + * We perform here some strictness analysis so that the + * iterator logic is bypassed entirely. + * + * @return \Iterator + */ + public function unwrap() + { + $iterator = $this->getInnerIterator(); + + if ($iterator instanceof CollectionInterface) { + $iterator = $iterator->unwrap(); + } + + if (get_class($iterator) !== ArrayIterator::class) { + return $this; + } + + // ArrayIterator can be traversed strictly. + // Let's do that for performance gains + + $callback = $this->_extractor; + $res = []; + + foreach ($iterator->getArrayCopy() as $k => $v) { + $res[$k] = $callback($v); + } + + return new ArrayIterator($res); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/FilterIterator.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/FilterIterator.php new file mode 100644 index 000000000..4612f2b9f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/FilterIterator.php @@ -0,0 +1,95 @@ +_callback = $callback; + $wrapper = new CallbackFilterIterator($items, $callback); + parent::__construct($wrapper); + } + + /** + * {@inheritDoc} + * + * We perform here some strictness analysis so that the + * iterator logic is bypassed entirely. + * + * @return \Iterator + */ + public function unwrap() + { + $filter = $this->getInnerIterator(); + $iterator = $filter->getInnerIterator(); + + if ($iterator instanceof CollectionInterface) { + $iterator = $iterator->unwrap(); + } + + if (get_class($iterator) !== ArrayIterator::class) { + return $filter; + } + + // ArrayIterator can be traversed strictly. + // Let's do that for performance gains + + $callback = $this->_callback; + $res = []; + + foreach ($iterator as $k => $v) { + if ($callback($v, $k, $iterator)) { + $res[$k] = $v; + } + } + + return new ArrayIterator($res); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/InsertIterator.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/InsertIterator.php new file mode 100644 index 000000000..89411169d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/InsertIterator.php @@ -0,0 +1,135 @@ +_path = $path; + $this->_target = $target; + $this->_values = $values; + } + + /** + * Advances the cursor to the next record + * + * @return void + */ + public function next() + { + parent::next(); + if ($this->_validValues) { + $this->_values->next(); + } + $this->_validValues = $this->_values->valid(); + } + + /** + * Returns the current element in the target collection after inserting + * the value from the source collection into the specified path. + * + * @return mixed + */ + public function current() + { + $row = parent::current(); + + if (!$this->_validValues) { + return $row; + } + + $pointer =& $row; + foreach ($this->_path as $step) { + if (!isset($pointer[$step])) { + return $row; + } + $pointer =& $pointer[$step]; + } + + $pointer[$this->_target] = $this->_values->current(); + + return $row; + } + + /** + * Resets the collection pointer. + * + * @return void + */ + public function rewind() + { + parent::rewind(); + $this->_values->rewind(); + $this->_validValues = $this->_values->valid(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/MapReduce.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/MapReduce.php new file mode 100644 index 000000000..c8b6090b2 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/MapReduce.php @@ -0,0 +1,193 @@ +emitIntermediate($value, $type); + * }; + * + * $reducer = function ($numbers, $type, $mr) { + * $mr->emit(array_unique($numbers), $type); + * }; + * $results = new MapReduce($data, $mapper, $reducer); + * ``` + * + * Previous example will generate the following result: + * + * ``` + * ['odd' => [1, 3, 5], 'even' => [2, 4]] + * ``` + * + * @param \Traversable $data the original data to be processed + * @param callable $mapper the mapper callback. This function will receive 3 arguments. + * The first one is the current value, second the current results key and third is + * this class instance so you can call the result emitters. + * @param callable|null $reducer the reducer callback. This function will receive 3 arguments. + * The first one is the list of values inside a bucket, second one is the name + * of the bucket that was created during the mapping phase and third one is an + * instance of this class. + */ + public function __construct(Traversable $data, callable $mapper, callable $reducer = null) + { + $this->_data = $data; + $this->_mapper = $mapper; + $this->_reducer = $reducer; + } + + /** + * Returns an iterator with the end result of running the Map and Reduce + * phases on the original data + * + * @return \ArrayIterator + */ + public function getIterator() + { + if (!$this->_executed) { + $this->_execute(); + } + + return new ArrayIterator($this->_result); + } + + /** + * Appends a new record to the bucket labelled with $key, usually as a result + * of mapping a single record from the original data. + * + * @param mixed $val The record itself to store in the bucket + * @param string $bucket the name of the bucket where to put the record + * @return void + */ + public function emitIntermediate($val, $bucket) + { + $this->_intermediate[$bucket][] = $val; + } + + /** + * Appends a new record to the final list of results and optionally assign a key + * for this record. + * + * @param mixed $val The value to be appended to the final list of results + * @param string|null $key and optional key to assign to the value + * @return void + */ + public function emit($val, $key = null) + { + $this->_result[$key === null ? $this->_counter : $key] = $val; + $this->_counter++; + } + + /** + * Runs the actual Map-Reduce algorithm. This is iterate the original data + * and call the mapper function for each , then for each intermediate + * bucket created during the Map phase call the reduce function. + * + * @return void + * @throws \LogicException if emitIntermediate was called but no reducer function + * was provided + */ + protected function _execute() + { + $mapper = $this->_mapper; + foreach ($this->_data as $key => $val) { + $mapper($val, $key, $this); + } + $this->_data = null; + + if (!empty($this->_intermediate) && empty($this->_reducer)) { + throw new LogicException('No reducer function was provided'); + } + + $reducer = $this->_reducer; + foreach ($this->_intermediate as $key => $list) { + $reducer($list, $key, $this); + } + $this->_intermediate = []; + $this->_executed = true; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/NestIterator.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/NestIterator.php new file mode 100644 index 000000000..ac9e6cb3b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/NestIterator.php @@ -0,0 +1,77 @@ +_nestKey = $nestKey; + } + + /** + * Returns a traversable containing the children for the current item + * + * @return \Traversable + */ + public function getChildren() + { + $property = $this->_propertyExtractor($this->_nestKey); + + return new static($property($this->current()), $this->_nestKey); + } + + /** + * Returns true if there is an array or a traversable object stored under the + * configured nestKey for the current item + * + * @return bool + */ + public function hasChildren() + { + $property = $this->_propertyExtractor($this->_nestKey); + $children = $property($this->current()); + + if (is_array($children)) { + return !empty($children); + } + + return $children instanceof Traversable; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/NoChildrenIterator.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/NoChildrenIterator.php new file mode 100644 index 000000000..aaf1fe6ac --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/NoChildrenIterator.php @@ -0,0 +1,47 @@ +_callback = $callback; + parent::__construct($items); + $this->_innerIterator = $this->getInnerIterator(); + } + + /** + * Returns the value returned by the callback after passing the current value in + * the iteration + * + * @return mixed + */ + public function current() + { + $callback = $this->_callback; + + return $callback(parent::current(), $this->key(), $this->_innerIterator); + } + + /** + * {@inheritDoc} + * + * We perform here some strictness analysis so that the + * iterator logic is bypassed entirely. + * + * @return \Iterator + */ + public function unwrap() + { + $iterator = $this->_innerIterator; + + if ($iterator instanceof CollectionInterface) { + $iterator = $iterator->unwrap(); + } + + if (get_class($iterator) !== ArrayIterator::class) { + return $this; + } + + // ArrayIterator can be traversed strictly. + // Let's do that for performance gains + + $callback = $this->_callback; + $res = []; + + foreach ($iterator as $k => $v) { + $res[$k] = $callback($v, $k, $iterator); + } + + return new ArrayIterator($res); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/SortIterator.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/SortIterator.php new file mode 100644 index 000000000..d9945d35c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/SortIterator.php @@ -0,0 +1,93 @@ +age; + * }); + * + * // output all user name order by their age in descending order + * foreach ($sorted as $user) { + * echo $user->name; + * } + * ``` + * + * This iterator does not preserve the keys passed in the original elements. + */ +class SortIterator extends Collection +{ + + /** + * Wraps this iterator around the passed items so when iterated they are returned + * in order. + * + * The callback will receive as first argument each of the elements in $items, + * the value returned in the callback will be used as the value for sorting such + * element. Please note that the callback function could be called more than once + * per element. + * + * @param array|\Traversable $items The values to sort + * @param callable|string $callback A function used to return the actual value to + * be compared. It can also be a string representing the path to use to fetch a + * column or property in each element + * @param int $dir either SORT_DESC or SORT_ASC + * @param int $type the type of comparison to perform, either SORT_STRING + * SORT_NUMERIC or SORT_NATURAL + */ + public function __construct($items, $callback, $dir = \SORT_DESC, $type = \SORT_NUMERIC) + { + if (!is_array($items)) { + $items = iterator_to_array((new Collection($items))->unwrap(), false); + } + + $callback = $this->_propertyExtractor($callback); + $results = []; + foreach ($items as $key => $val) { + $val = $callback($val); + if ($val instanceof DateTimeInterface && $type === \SORT_NUMERIC) { + $val = $val->format('U'); + } + $results[$key] = $val; + } + + $dir === SORT_DESC ? arsort($results, $type) : asort($results, $type); + + foreach (array_keys($results) as $key) { + $results[$key] = $items[$key]; + } + parent::__construct($results); + } + + /** + * {@inheritDoc} + * + * @return \Iterator + */ + public function unwrap() + { + return $this->getInnerIterator(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/StoppableIterator.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/StoppableIterator.php new file mode 100644 index 000000000..7d1882c6c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/StoppableIterator.php @@ -0,0 +1,119 @@ +_condition = $condition; + parent::__construct($items); + $this->_innerIterator = $this->getInnerIterator(); + } + + /** + * Evaluates the condition and returns its result, this controls + * whether or not more results will be yielded. + * + * @return bool + */ + public function valid() + { + if (!parent::valid()) { + return false; + } + + $current = $this->current(); + $key = $this->key(); + $condition = $this->_condition; + + return !$condition($current, $key, $this->_innerIterator); + } + + /** + * {@inheritDoc} + * + * We perform here some strictness analysis so that the + * iterator logic is bypassed entirely. + * + * @return \Iterator + */ + public function unwrap() + { + $iterator = $this->_innerIterator; + + if ($iterator instanceof CollectionInterface) { + $iterator = $iterator->unwrap(); + } + + if (get_class($iterator) !== ArrayIterator::class) { + return $this; + } + + // ArrayIterator can be traversed strictly. + // Let's do that for performance gains + + $callback = $this->_condition; + $res = []; + + foreach ($iterator as $k => $v) { + if ($callback($v, $k, $iterator)) { + break; + } + $res[$k] = $v; + } + + return new ArrayIterator($res); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/TreeIterator.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/TreeIterator.php new file mode 100644 index 000000000..f4d22516f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/TreeIterator.php @@ -0,0 +1,105 @@ +_mode = $mode; + } + + /** + * Returns another iterator which will return the values ready to be displayed + * to a user. It does so by extracting one property from each of the elements + * and prefixing it with a spacer so that the relative position in the tree + * can be visualized. + * + * Both $valuePath and $keyPath can be a string with a property name to extract + * or a dot separated path of properties that should be followed to get the last + * one in the path. + * + * Alternatively, $valuePath and $keyPath can be callable functions. They will get + * the current element as first parameter, the current iteration key as second + * parameter, and the iterator instance as third argument. + * + * ### Example + * + * ``` + * $printer = (new Collection($treeStructure))->listNested()->printer('name'); + * ``` + * + * Using a closure: + * + * ``` + * $printer = (new Collection($treeStructure)) + * ->listNested() + * ->printer(function ($item, $key, $iterator) { + * return $item->name; + * }); + * ``` + * + * @param string|callable $valuePath The property to extract or a callable to return + * the display value + * @param string|callable|null $keyPath The property to use as iteration key or a + * callable returning the key value. + * @param string $spacer The string to use for prefixing the values according to + * their depth in the tree + * @return \Cake\Collection\Iterator\TreePrinter + */ + public function printer($valuePath, $keyPath = null, $spacer = '__') + { + if (!$keyPath) { + $counter = 0; + $keyPath = function () use (&$counter) { + return $counter++; + }; + } + + return new TreePrinter( + $this->getInnerIterator(), + $valuePath, + $keyPath, + $spacer, + $this->_mode + ); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/TreePrinter.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/TreePrinter.php new file mode 100644 index 000000000..fa72d0eab --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/TreePrinter.php @@ -0,0 +1,127 @@ +_value = $this->_propertyExtractor($valuePath); + $this->_key = $this->_propertyExtractor($keyPath); + $this->_spacer = $spacer; + } + + /** + * Returns the current iteration key + * + * @return mixed + */ + public function key() + { + $extractor = $this->_key; + + return $extractor($this->_fetchCurrent(), parent::key(), $this); + } + + /** + * Returns the current iteration value + * + * @return string + */ + public function current() + { + $extractor = $this->_value; + $current = $this->_fetchCurrent(); + $spacer = str_repeat($this->_spacer, $this->getDepth()); + + return $spacer . $extractor($current, parent::key(), $this); + } + + /** + * Advances the cursor one position + * + * @return void + */ + public function next() + { + parent::next(); + $this->_current = null; + } + + /** + * Returns the current iteration element and caches its value + * + * @return mixed + */ + protected function _fetchCurrent() + { + if ($this->_current !== null) { + return $this->_current; + } + + return $this->_current = parent::current(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/UnfoldIterator.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/UnfoldIterator.php new file mode 100644 index 000000000..06a2ca6a6 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/UnfoldIterator.php @@ -0,0 +1,86 @@ +_unfolder = $unfolder; + parent::__construct($items); + $this->_innerIterator = $this->getInnerIterator(); + } + + /** + * Returns true as each of the elements in the array represent a + * list of items + * + * @return bool + */ + public function hasChildren() + { + return true; + } + + /** + * Returns an iterator containing the items generated by transforming + * the current value with the callable function. + * + * @return \RecursiveIterator + */ + public function getChildren() + { + $current = $this->current(); + $key = $this->key(); + $unfolder = $this->_unfolder; + + return new NoChildrenIterator($unfolder($current, $key, $this->_innerIterator)); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/Iterator/ZipIterator.php b/app/vendor/cakephp/cakephp/src/Collection/Iterator/ZipIterator.php new file mode 100644 index 000000000..9c4adac0a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/Iterator/ZipIterator.php @@ -0,0 +1,126 @@ +toList(); // Returns [[1, 3], [2, 4]] + * ``` + * + * You can also chose a custom function to zip the elements together, such + * as doing a sum by index: + * + * ### Example + * + * ``` + * $iterator = new ZipIterator([[1, 2], [3, 4]], function ($a, $b) { + * return $a + $b; + * }); + * $iterator->toList(); // Returns [4, 6] + * ``` + */ +class ZipIterator extends MultipleIterator implements CollectionInterface, Serializable +{ + + use CollectionTrait; + + /** + * The function to use for zipping items together + * + * @var callable + */ + protected $_callback; + + /** + * Contains the original iterator objects that were attached + * + * @var array + */ + protected $_iterators = []; + + /** + * Creates the iterator to merge together the values by for all the passed + * iterators by their corresponding index. + * + * @param array $sets The list of array or iterators to be zipped. + * @param callable|null $callable The function to use for zipping the elements of each iterator. + */ + public function __construct(array $sets, $callable = null) + { + $sets = array_map(function ($items) { + return (new Collection($items))->unwrap(); + }, $sets); + + $this->_callback = $callable; + parent::__construct(MultipleIterator::MIT_NEED_ALL | MultipleIterator::MIT_KEYS_NUMERIC); + + foreach ($sets as $set) { + $this->_iterators[] = $set; + $this->attachIterator($set); + } + } + + /** + * Returns the value resulting out of zipping all the elements for all the + * iterators with the same positional index. + * + * @return mixed + */ + public function current() + { + if ($this->_callback === null) { + return parent::current(); + } + + return call_user_func_array($this->_callback, parent::current()); + } + + /** + * Returns a string representation of this object that can be used + * to reconstruct it + * + * @return string + */ + public function serialize() + { + return serialize($this->_iterators); + } + + /** + * Unserializes the passed string and rebuilds the ZipIterator instance + * + * @param string $iterators The serialized iterators + * @return void + */ + public function unserialize($iterators) + { + parent::__construct(MultipleIterator::MIT_NEED_ALL | MultipleIterator::MIT_KEYS_NUMERIC); + $this->_iterators = unserialize($iterators); + foreach ($this->_iterators as $it) { + $this->attachIterator($it); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Collection/LICENSE.txt new file mode 100644 index 000000000..0c4b7932c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2016, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/vendor/cakephp/cakephp/src/Collection/README.md b/app/vendor/cakephp/cakephp/src/Collection/README.md new file mode 100644 index 000000000..986391317 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/README.md @@ -0,0 +1,31 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/collection.svg?style=flat-square)](https://packagist.org/packages/cakephp/collection) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# CakePHP Collection Library + +The collection classes provide a set of tools to manipulate arrays or Traversable objects. +If you have ever used underscore.js, you have an idea of what you can expect from the collection classes. + +## Usage + +Collections can be created using an array or Traversable object. A simple use of a Collection would be: + +```php +use Cake\Collection\Collection; + +$items = ['a' => 1, 'b' => 2, 'c' => 3]; +$collection = new Collection($items); + +// Create a new collection containing elements +// with a value greater than one. +$overOne = $collection->filter(function ($value, $key, $iterator) { + return $value > 1; +}); +``` + +The `Collection\CollectionTrait` allows you to integrate collection-like features into any Traversable object +you have in your application as well. + +## Documentation + +Please make sure you check the [official documentation](https://book.cakephp.org/3.0/en/core-libraries/collections.html) diff --git a/app/vendor/cakephp/cakephp/src/Collection/composer.json b/app/vendor/cakephp/cakephp/src/Collection/composer.json new file mode 100644 index 000000000..47a24c47f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/composer.json @@ -0,0 +1,36 @@ +{ + "name": "cakephp/collection", + "description": "Work easily with arrays and iterators by having a battery of utility traversal methods", + "type": "library", + "keywords": [ + "cakephp", + "collections", + "iterators", + "arrays" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/collection/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/collection" + }, + "require": { + "php": ">=5.6.0" + }, + "autoload": { + "psr-4": { + "Cake\\Collection\\": "." + }, + "files": [ + "functions.php" + ] + } +} diff --git a/app/vendor/cakephp/cakephp/src/Collection/functions.php b/app/vendor/cakephp/cakephp/src/Collection/functions.php new file mode 100644 index 000000000..e1f01b584 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Collection/functions.php @@ -0,0 +1,29 @@ +commands = $commands; + } + + /** + * Main function Prints out the list of shells. + * + * @param \Cake\Console\Arguments $args The command arguments. + * @param \Cake\Console\ConsoleIo $io The console io + * @return int + */ + public function execute(Arguments $args, ConsoleIo $io) + { + if (!$args->getOption('xml')) { + $io->out('Current Paths:', 2); + $io->out('* app: ' . APP_DIR); + $io->out('* root: ' . rtrim(ROOT, DIRECTORY_SEPARATOR)); + $io->out('* core: ' . rtrim(CORE_PATH, DIRECTORY_SEPARATOR)); + $io->out(''); + + $io->out('Available Commands:', 2); + } + + $commands = $this->commands->getIterator(); + $commands->ksort(); + + if ($args->getOption('xml')) { + $this->asXml($io, $commands); + + return static::CODE_SUCCESS; + } + $this->asText($io, $commands); + + return static::CODE_SUCCESS; + } + + /** + * Output text. + * + * @param \Cake\Console\ConsoleIo $io The console io + * @param \ArrayIterator $commands The command collection to output. + * @return void + */ + protected function asText($io, $commands) + { + $invert = []; + foreach ($commands as $name => $class) { + if (is_object($class)) { + $class = get_class($class); + } + if (!isset($invert[$class])) { + $invert[$class] = []; + } + $invert[$class][] = $name; + } + + foreach ($commands as $name => $class) { + if (is_object($class)) { + $class = get_class($class); + } + if (count($invert[$class]) == 1) { + $io->out('- ' . $name); + } + + if (count($invert[$class]) > 1) { + // Sort by length so we can get the shortest name. + usort($invert[$class], function ($a, $b) { + return strlen($a) - strlen($b); + }); + $io->out('- ' . array_shift($invert[$class])); + + // Empty the list to prevent duplicates + $invert[$class] = []; + } + } + $io->out(''); + + $io->out('To run a command, type `cake shell_name [args|options]`'); + $io->out('To get help on a specific command, type `cake shell_name --help`', 2); + } + + /** + * Output as XML + * + * @param \Cake\Console\ConsoleIo $io The console io + * @param \ArrayIterator $commands The command collection to output + * @return void + */ + protected function asXml($io, $commands) + { + $shells = new SimpleXMLElement(''); + foreach ($commands as $name => $class) { + if (is_object($class)) { + $class = get_class($class); + } + $shell = $shells->addChild('shell'); + $shell->addAttribute('name', $name); + $shell->addAttribute('call_as', $name); + $shell->addAttribute('provider', $class); + $shell->addAttribute('help', $name . ' -h'); + } + $io->setOutputAs(ConsoleOutput::RAW); + $io->out($shells->saveXML()); + } + + /** + * Gets the option parser instance and configures it. + * + * @param \Cake\Console\ConsoleOptionParser $parser The parser to build + * @return \Cake\Console\ConsoleOptionParser + */ + protected function buildOptionParser(ConsoleOptionParser $parser) + { + $parser->setDescription( + 'Get the list of available shells for this application.' + )->addOption('xml', [ + 'help' => 'Get the listing as XML.', + 'boolean' => true + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Command/VersionCommand.php b/app/vendor/cakephp/cakephp/src/Command/VersionCommand.php new file mode 100644 index 000000000..457cbce1b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Command/VersionCommand.php @@ -0,0 +1,40 @@ +out(Configure::version()); + + return static::CODE_SUCCESS; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/Arguments.php b/app/vendor/cakephp/cakephp/src/Console/Arguments.php new file mode 100644 index 000000000..6d7035d99 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/Arguments.php @@ -0,0 +1,162 @@ +args = $args; + $this->options = $options; + $this->argNames = $argNames; + } + + /** + * Get all positional arguments. + * + * @return string[] + */ + public function getArguments() + { + return $this->args; + } + + /** + * Get positional arguments by index. + * + * @param int $index The argument index to access. + * @return string|null The argument value or null + */ + public function getArgumentAt($index) + { + if ($this->hasArgumentAt($index)) { + return $this->args[$index]; + } + + return null; + } + + /** + * Check if a positional argument exists + * + * @param int $index The argument index to check. + * @return bool + */ + public function hasArgumentAt($index) + { + return isset($this->args[$index]); + } + + /** + * Check if a positional argument exists by name + * + * @param string $name The argument name to check. + * @return bool + */ + public function hasArgument($name) + { + $offset = array_search($name, $this->argNames, true); + if ($offset === false) { + return false; + } + + return isset($this->args[$offset]); + } + + /** + * Check if a positional argument exists by name + * + * @param string $name The argument name to check. + * @return string|null + */ + public function getArgument($name) + { + $offset = array_search($name, $this->argNames, true); + if ($offset === false) { + return null; + } + + return $this->args[$offset]; + } + + /** + * Get an array of all the options + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Get an option's value or null + * + * @param string $name The name of the option to check. + * @return string|int|bool|null The option value or null. + */ + public function getOption($name) + { + if (isset($this->options[$name])) { + return $this->options[$name]; + } + + return null; + } + + /** + * Check if an option is defined and not null. + * + * @param string $name The name of the option to check. + * @return bool + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/Command.php b/app/vendor/cakephp/cakephp/src/Console/Command.php new file mode 100644 index 000000000..91fbde1f5 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/Command.php @@ -0,0 +1,248 @@ +modelFactory('Table', function ($alias) { + return $this->getTableLocator()->get($alias); + }); + } + + /** + * Set the name this command uses in the collection. + * + * Generally invoked by the CommandCollection when the command is added. + * Required to have at least one space in the name so that the root + * command can be calculated. + * + * @param string $name The name the command uses in the collection. + * @return $this + * @throws \InvalidArgumentException + */ + public function setName($name) + { + if (strpos($name, ' ') < 1) { + throw new InvalidArgumentException( + "The name '{$name}' is missing a space. Names should look like `cake routes`" + ); + } + $this->name = $name; + + return $this; + } + + /** + * Get the command name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the option parser. + * + * You can override buildOptionParser() to define your options & arguments. + * + * @return \Cake\Console\ConsoleOptionParser + * @throws \RuntimeException When the parser is invalid + */ + public function getOptionParser() + { + list($root, $name) = explode(' ', $this->name, 2); + $parser = new ConsoleOptionParser($name); + $parser->setRootName($root); + + $parser = $this->buildOptionParser($parser); + if (!($parser instanceof ConsoleOptionParser)) { + throw new RuntimeException(sprintf( + "Invalid option parser returned from buildOptionParser(). Expected %s, got %s", + ConsoleOptionParser::class, + getTypeName($parser) + )); + } + + return $parser; + } + + /** + * Hook method for defining this command's option parser. + * + * @param \Cake\Console\ConsoleOptionParser $parser The parser to be defined + * @return \Cake\Console\ConsoleOptionParser The built parser. + */ + protected function buildOptionParser(ConsoleOptionParser $parser) + { + return $parser; + } + + /** + * Hook method invoked by CakePHP when a command is about to be executed. + * + * Override this method and implement expensive/important setup steps that + * should not run on every command run. This method will be called *before* + * the options and arguments are validated and processed. + * + * @return void + */ + public function initialize() + { + } + + /** + * Run the command. + * + * @param array $argv Arguments from the CLI environment. + * @param \Cake\Console\ConsoleIo $io The console io + * @return int|null Exit code or null for success. + */ + public function run(array $argv, ConsoleIo $io) + { + $this->initialize(); + + $parser = $this->getOptionParser(); + try { + list($options, $arguments) = $parser->parse($argv); + $args = new Arguments( + $arguments, + $options, + $parser->argumentNames() + ); + } catch (ConsoleException $e) { + $io->err('Error: ' . $e->getMessage()); + + return static::CODE_ERROR; + } + $this->setOutputLevel($args, $io); + + if ($args->getOption('help')) { + $this->displayHelp($parser, $args, $io); + + return static::CODE_SUCCESS; + } + + return $this->execute($args, $io); + } + + /** + * Output help content + * + * @param \Cake\Console\ConsoleOptionParser $parser The option parser. + * @param \Cake\Console\Arguments $args The command arguments. + * @param \Cake\Console\ConsoleIo $io The console io + * @return void + */ + protected function displayHelp(ConsoleOptionParser $parser, Arguments $args, ConsoleIo $io) + { + $format = 'text'; + if ($args->getArgumentAt(0) === 'xml') { + $format = 'xml'; + $io->setOutputAs(ConsoleOutput::RAW); + } + + $io->out($parser->help(null, $format)); + } + + /** + * Set the output level based on the Arguments. + * + * @param \Cake\Console\Arguments $args The command arguments. + * @param \Cake\Console\ConsoleIo $io The console io + * @return void + */ + protected function setOutputLevel(Arguments $args, ConsoleIo $io) + { + $io->setLoggers(ConsoleIo::NORMAL); + if ($args->getOption('quiet')) { + $io->level(ConsoleIo::QUIET); + $io->setLoggers(ConsoleIo::QUIET); + } + if ($args->getOption('verbose')) { + $io->level(ConsoleIo::VERBOSE); + $io->setLoggers(ConsoleIo::VERBOSE); + } + } + + /** + * Implement this method with your command's logic. + * + * @param \Cake\Console\Arguments $args The command arguments. + * @param \Cake\Console\ConsoleIo $io The console io + * @return null|int The exit code or null for success + */ + public function execute(Arguments $args, ConsoleIo $io) + { + return null; + } + + /** + * Halt the the current process with a StopException. + * + * @param int $code The exit code to use. + * @throws \Cake\Console\Exception\StopException + * @return void + */ + public function abort($code = self::CODE_ERROR) + { + throw new StopException('Command aborted', $code); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/CommandCollection.php b/app/vendor/cakephp/cakephp/src/Console/CommandCollection.php new file mode 100644 index 000000000..294ca73fd --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/CommandCollection.php @@ -0,0 +1,222 @@ + $command) { + $this->add($name, $command); + } + } + + /** + * Add a command to the collection + * + * @param string $name The name of the command you want to map. + * @param string|\Cake\Console\Shell|\Cake\Console\Command $command The command to map. + * @return $this + */ + public function add($name, $command) + { + // Once we have a new Command class this should check + // against that interface. + if (!is_subclass_of($command, Shell::class) && !is_subclass_of($command, Command::class)) { + $class = is_string($command) ? $command : get_class($command); + throw new InvalidArgumentException( + "Cannot use '$class' for command '$name' it is not a subclass of Cake\Console\Shell or Cake\Console\Command." + ); + } + + $this->commands[$name] = $command; + + return $this; + } + + /** + * Add multiple commands at once. + * + * @param array $commands A map of command names => command classes/instances. + * @return $this + * @see \Cake\Console\CommandCollection::add() + */ + public function addMany(array $commands) + { + foreach ($commands as $name => $class) { + $this->add($name, $class); + } + + return $this; + } + + /** + * Remove a command from the collection if it exists. + * + * @param string $name The named shell. + * @return $this + */ + public function remove($name) + { + unset($this->commands[$name]); + + return $this; + } + + /** + * Check whether the named shell exists in the collection. + * + * @param string $name The named shell. + * @return bool + */ + public function has($name) + { + return isset($this->commands[$name]); + } + + /** + * Get the target for a command. + * + * @param string $name The named shell. + * @return string|\Cake\Console\Shell Either the shell class or an instance. + * @throws \InvalidArgumentException when unknown commands are fetched. + */ + public function get($name) + { + if (!$this->has($name)) { + throw new InvalidArgumentException("The $name is not a known command name."); + } + + return $this->commands[$name]; + } + + /** + * Implementation of IteratorAggregate. + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->commands); + } + + /** + * Implementation of Countable. + * + * Get the number of commands in the collection. + * + * @return int + */ + public function count() + { + return count($this->commands); + } + + /** + * Auto-discover shell & commands from the named plugin. + * + * Discovered commands will have their names de-duplicated with + * existing commands in the collection. If a command is already + * defined in the collection and discovered in a plugin, only + * the long name (`plugin.command`) will be returned. + * + * @param string $plugin The plugin to scan. + * @return array Discovered plugin commands. + */ + public function discoverPlugin($plugin) + { + $scanner = new CommandScanner(); + $shells = $scanner->scanPlugin($plugin); + + return $this->resolveNames($shells); + } + + /** + * Resolve names based on existing commands + * + * @param array $input The results of a CommandScanner operation. + * @return array A flat map of command names => class names. + */ + protected function resolveNames(array $input) + { + $out = []; + foreach ($input as $info) { + $name = $info['name']; + $addLong = $name !== $info['fullName']; + + // If the short name has been used, use the full name. + // This allows app shells to have name preference. + // and app shells to overwrite core shells. + if ($this->has($name) && $addLong) { + $name = $info['fullName']; + } + + $out[$name] = $info['class']; + if ($addLong) { + $out[$info['fullName']] = $info['class']; + } + } + + return $out; + } + + /** + * Automatically discover shell commands in CakePHP, the application and all plugins. + * + * Commands will be located using filesystem conventions. Commands are + * discovered in the following order: + * + * - CakePHP provided commands + * - Application commands + * + * Commands defined in the application will ovewrite commands with + * the same name provided by CakePHP. + * + * @return array An array of command names and their classes. + */ + public function autoDiscover() + { + $scanner = new CommandScanner(); + + $core = $this->resolveNames($scanner->scanCore()); + $app = $this->resolveNames($scanner->scanApp()); + + return array_merge($core, $app); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/CommandCollectionAwareInterface.php b/app/vendor/cakephp/cakephp/src/Console/CommandCollectionAwareInterface.php new file mode 100644 index 000000000..89a2ade6e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/CommandCollectionAwareInterface.php @@ -0,0 +1,31 @@ +app = $app; + $this->root = $root; + $this->factory = $factory ?: new CommandFactory(); + $this->aliases = [ + '--version' => 'version', + '--help' => 'help', + '-h' => 'help', + ]; + } + + /** + * Replace the entire alias map for a runner. + * + * Aliases allow you to define alternate names for commands + * in the collection. This can be useful to add top level switches + * like `--version` or `-h` + * + * ### Usage + * + * ``` + * $runner->setAliases(['--version' => 'version']); + * ``` + * + * @param array $aliases The map of aliases to replace. + * @return $this + */ + public function setAliases(array $aliases) + { + $this->aliases = $aliases; + + return $this; + } + + /** + * Run the command contained in $argv. + * + * Use the application to do the following: + * + * - Bootstrap the application + * - Create the CommandCollection using the console() hook on the application. + * - Trigger the `Console.buildCommands` event of auto-wiring plugins. + * - Run the requested command. + * + * @param array $argv The arguments from the CLI environment. + * @param \Cake\Console\ConsoleIo $io The ConsoleIo instance. Used primarily for testing. + * @return int The exit code of the command. + * @throws \RuntimeException + */ + public function run(array $argv, ConsoleIo $io = null) + { + $this->bootstrap(); + + $commands = new CommandCollection([ + 'version' => VersionCommand::class, + 'help' => HelpCommand::class, + ]); + $commands = $this->app->console($commands); + $this->checkCollection($commands, 'console'); + + if ($this->app instanceof PluginApplicationInterface) { + $commands = $this->app->pluginConsole($commands); + } + $this->checkCollection($commands, 'pluginConsole'); + $this->dispatchEvent('Console.buildCommands', ['commands' => $commands]); + $this->loadRoutes(); + + if (empty($argv)) { + throw new RuntimeException("Cannot run any commands. No arguments received."); + } + // Remove the root executable segment + array_shift($argv); + + $io = $io ?: new ConsoleIo(); + $name = $this->resolveName($commands, $io, array_shift($argv)); + + $result = Shell::CODE_ERROR; + $shell = $this->getShell($io, $commands, $name); + if ($shell instanceof Shell) { + $result = $this->runShell($shell, $argv); + } + if ($shell instanceof Command) { + $result = $shell->run($argv, $io); + } + + if ($result === null || $result === true) { + return Shell::CODE_SUCCESS; + } + if (is_int($result)) { + return $result; + } + + return Shell::CODE_ERROR; + } + + /** + * Application bootstrap wrapper. + * + * Calls `bootstrap()` and `events()` if application implements `EventApplicationInterface`. + * After the application is bootstrapped and events are attached, plugins are bootstrapped + * and have their events attached. + * + * @return void + */ + protected function bootstrap() + { + $this->app->bootstrap(); + if ($this->app instanceof PluginApplicationInterface) { + $this->app->pluginBootstrap(); + } + } + + /** + * Check the created CommandCollection + * + * @param mixed $commands The CommandCollection to check, could be anything though. + * @param string $method The method that was used. + * @return void + * @throws \RuntimeException + * @deprecated 3.6.0 This method should be replaced with return types in 4.x + */ + protected function checkCollection($commands, $method) + { + if (!($commands instanceof CommandCollection)) { + $type = getTypeName($commands); + throw new RuntimeException( + "The application's `{$method}` method did not return a CommandCollection." . + " Got '{$type}' instead." + ); + } + } + + /** + * Get the application's event manager or the global one. + * + * @return \Cake\Event\EventManagerInterface + */ + public function getEventManager() + { + if ($this->app instanceof PluginApplicationInterface) { + return $this->app->getEventManager(); + } + + return EventManager::instance(); + } + + /** + * Get/set the application's event manager. + * + * If the application does not support events and this method is used as + * a setter, an exception will be raised. + * + * @param \Cake\Event\EventManager|null $events The event manager to set. + * @return \Cake\Event\EventManager|$this + * @deprecated 3.6.0 Will be removed in 4.0 + */ + public function eventManager(EventManager $events = null) + { + deprecationWarning('eventManager() is deprecated. Use getEventManager()/setEventManager() instead.'); + if ($events === null) { + return $this->getEventManager(); + } + + return $this->setEventManager($events); + } + + /** + * Get/set the application's event manager. + * + * If the application does not support events and this method is used as + * a setter, an exception will be raised. + * + * @param \Cake\Event\EventManager $events The event manager to set. + * @return $this + */ + public function setEventManager(EventManager $events) + { + if ($this->app instanceof PluginApplicationInterface) { + $this->app->setEventManager($events); + + return $this; + } + + throw new InvalidArgumentException('Cannot set the event manager, the application does not support events.'); + } + + /** + * Get the shell instance for a given command name + * + * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class. + * @param \Cake\Console\CommandCollection $commands The command collection to find the shell in. + * @param string $name The command name to find + * @return \Cake\Console\Shell|\Cake\Console\Command + */ + protected function getShell(ConsoleIo $io, CommandCollection $commands, $name) + { + $instance = $commands->get($name); + if (is_string($instance)) { + $instance = $this->createShell($instance, $io); + } + if ($instance instanceof Shell) { + $instance->setRootName($this->root); + } + if ($instance instanceof Command) { + $instance->setName("{$this->root} {$name}"); + } + if ($instance instanceof CommandCollectionAwareInterface) { + $instance->setCommandCollection($commands); + } + + return $instance; + } + + /** + * Resolve the command name into a name that exists in the collection. + * + * Apply backwards compatibile inflections and aliases. + * + * @param \Cake\Console\CommandCollection $commands The command collection to check. + * @param \Cake\Console\ConsoleIo $io ConsoleIo object for errors. + * @param string $name The name from the CLI args. + * @return string The resolved name. + */ + protected function resolveName($commands, $io, $name) + { + if (!$name) { + $io->err('No command provided. Choose one of the available commands.', 2); + $name = 'help'; + } + if (isset($this->aliases[$name])) { + $name = $this->aliases[$name]; + } + if (!$commands->has($name)) { + $name = Inflector::underscore($name); + } + if (!$commands->has($name)) { + throw new RuntimeException( + "Unknown command `{$this->root} {$name}`." . + " Run `{$this->root} --help` to get the list of valid commands." + ); + } + + return $name; + } + + /** + * Execute a Shell class. + * + * @param \Cake\Console\Shell $shell The shell to run. + * @param array $argv The CLI arguments to invoke. + * @return int Exit code + */ + protected function runShell(Shell $shell, array $argv) + { + try { + $shell->initialize(); + + return $shell->runCommand($argv, true); + } catch (StopException $e) { + return $e->getCode(); + } + } + + /** + * The wrapper for creating shell instances. + * + * @param string $className Shell class name. + * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class. + * @return \Cake\Console\Shell|\Cake\Console\Command + */ + protected function createShell($className, ConsoleIo $io) + { + $shell = $this->factory->create($className); + if ($shell instanceof Shell) { + $shell->setIo($io); + } + + return $shell; + } + + /** + * Ensure that the application's routes are loaded. + * + * Console commands and shells often need to generate URLs. + * + * @return void + */ + protected function loadRoutes() + { + $builder = Router::createRouteBuilder('/'); + + if ($this->app instanceof HttpApplicationInterface) { + $this->app->routes($builder); + } + if ($this->app instanceof PluginApplicationInterface) { + $this->app->pluginRoutes($builder); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/CommandScanner.php b/app/vendor/cakephp/cakephp/src/Console/CommandScanner.php new file mode 100644 index 000000000..b8f86db05 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/CommandScanner.php @@ -0,0 +1,148 @@ +scanDir( + dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Shell' . DIRECTORY_SEPARATOR, + 'Cake\Shell\\', + '', + ['command_list'] + ); + $coreCommands = $this->scanDir( + dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Command' . DIRECTORY_SEPARATOR, + 'Cake\Command\\', + '', + ['command_list'] + ); + + return array_merge($coreShells, $coreCommands); + } + + /** + * Scan the application for shells & commands. + * + * @return array A list of command metadata. + */ + public function scanApp() + { + $appNamespace = Configure::read('App.namespace'); + $appShells = $this->scanDir( + App::path('Shell')[0], + $appNamespace . '\Shell\\', + '', + [] + ); + $appCommands = $this->scanDir( + App::path('Command')[0], + $appNamespace . '\Command\\', + '', + [] + ); + + return array_merge($appShells, $appCommands); + } + + /** + * Scan the named plugin for shells and commands + * + * @param string $plugin The named plugin. + * @return array A list of command metadata. + */ + public function scanPlugin($plugin) + { + if (!Plugin::loaded($plugin)) { + return []; + } + $path = Plugin::classPath($plugin); + $namespace = str_replace('/', '\\', $plugin); + $prefix = Inflector::underscore($plugin) . '.'; + + $commands = $this->scanDir($path . 'Command', $namespace . '\Command\\', $prefix, []); + $shells = $this->scanDir($path . 'Shell', $namespace . '\Shell\\', $prefix, []); + + return array_merge($shells, $commands); + } + + /** + * Scan a directory for .php files and return the class names that + * should be within them. + * + * @param string $path The directory to read. + * @param string $namespace The namespace the shells live in. + * @param string $prefix The prefix to apply to commands for their full name. + * @param array $hide A list of command names to hide as they are internal commands. + * @return array The list of shell info arrays based on scanning the filesystem and inflection. + */ + protected function scanDir($path, $namespace, $prefix, array $hide) + { + $dir = new Folder($path); + $contents = $dir->read(true, true); + if (empty($contents[1])) { + return []; + } + + $classPattern = '/(Shell|Command)$/'; + $shells = []; + foreach ($contents[1] as $file) { + if (substr($file, -4) !== '.php') { + continue; + } + $shell = substr($file, 0, -4); + if (!preg_match($classPattern, $shell)) { + continue; + } + + $name = Inflector::underscore(preg_replace($classPattern, '', $shell)); + if (in_array($name, $hide, true)) { + continue; + } + + $class = $namespace . $shell; + if (!is_subclass_of($class, Shell::class) && !is_subclass_of($class, Command::class)) { + continue; + } + + $shells[] = [ + 'file' => $path . $file, + 'fullName' => $prefix . $name, + 'name' => $name, + 'class' => $class + ]; + } + + return $shells; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/ConsoleErrorHandler.php b/app/vendor/cakephp/cakephp/src/Console/ConsoleErrorHandler.php new file mode 100644 index 000000000..798cffa2c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/ConsoleErrorHandler.php @@ -0,0 +1,137 @@ +_stderr = $options['stderr']; + $this->_options = $options; + } + + /** + * Handle errors in the console environment. Writes errors to stderr, + * and logs messages if Configure::read('debug') is false. + * + * @param \Exception $exception Exception instance. + * @return void + * @throws \Exception When renderer class not found + * @see https://secure.php.net/manual/en/function.set-exception-handler.php + */ + public function handleException(Exception $exception) + { + $this->_displayException($exception); + $this->_logException($exception); + $code = $exception->getCode(); + $code = ($code && is_int($code)) ? $code : 1; + $this->_stop($code); + } + + /** + * Prints an exception to stderr. + * + * @param \Exception $exception The exception to handle + * @return void + */ + protected function _displayException($exception) + { + $errorName = 'Exception:'; + if ($exception instanceof FatalErrorException) { + $errorName = 'Fatal Error:'; + } + + if ($exception instanceof PHP7ErrorException) { + $exception = $exception->getError(); + } + + $message = sprintf( + '%s %s in [%s, line %s]', + $errorName, + $exception->getMessage(), + $exception->getFile(), + $exception->getLine() + ); + $this->_stderr->write($message); + } + + /** + * Prints an error to stderr. + * + * Template method of BaseErrorHandler. + * + * @param array $error An array of error data. + * @param bool $debug Whether or not the app is in debug mode. + * @return void + */ + protected function _displayError($error, $debug) + { + $message = sprintf( + '%s in [%s, line %s]', + $error['description'], + $error['file'], + $error['line'] + ); + $message = sprintf( + "%s Error: %s\n", + $error['error'], + $message + ); + $this->_stderr->write($message); + } + + /** + * Stop the execution and set the exit code for the process. + * + * @param int $code The exit code. + * @return void + */ + protected function _stop($code) + { + exit($code); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/ConsoleInput.php b/app/vendor/cakephp/cakephp/src/Console/ConsoleInput.php new file mode 100644 index 000000000..118cc1fea --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/ConsoleInput.php @@ -0,0 +1,84 @@ +_canReadline = (extension_loaded('readline') && $handle === 'php://stdin'); + $this->_input = fopen($handle, 'rb'); + } + + /** + * Read a value from the stream + * + * @return mixed The value of the stream + */ + public function read() + { + if ($this->_canReadline) { + $line = readline(''); + if (strlen($line) > 0) { + readline_add_history($line); + } + + return $line; + } + + return fgets($this->_input); + } + + /** + * Check if data is available on stdin + * + * @param int $timeout An optional time to wait for data + * @return bool True for data available, false otherwise + */ + public function dataAvailable($timeout = 0) + { + $readFds = [$this->_input]; + $readyFds = stream_select($readFds, $writeFds, $errorFds, $timeout); + + return ($readyFds > 0); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/ConsoleInputArgument.php b/app/vendor/cakephp/cakephp/src/Console/ConsoleInputArgument.php new file mode 100644 index 000000000..bc0314dfd --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/ConsoleInputArgument.php @@ -0,0 +1,197 @@ + $value) { + $this->{'_' . $key} = $value; + } + } else { + $this->_name = $name; + $this->_help = $help; + $this->_required = $required; + $this->_choices = $choices; + } + } + + /** + * Get the value of the name attribute. + * + * @return string Value of this->_name. + */ + public function name() + { + return $this->_name; + } + + /** + * Checks if this argument is equal to another argument. + * + * @param \Cake\Console\ConsoleInputArgument $argument ConsoleInputArgument to compare to. + * @return bool + */ + public function isEqualTo(ConsoleInputArgument $argument) + { + return $this->usage() === $argument->usage(); + } + + /** + * Generate the help for this argument. + * + * @param int $width The width to make the name of the option. + * @return string + */ + public function help($width = 0) + { + $name = $this->_name; + if (strlen($name) < $width) { + $name = str_pad($name, $width, ' '); + } + $optional = ''; + if (!$this->isRequired()) { + $optional = ' (optional)'; + } + if ($this->_choices) { + $optional .= sprintf(' (choices: %s)', implode('|', $this->_choices)); + } + + return sprintf('%s%s%s', $name, $this->_help, $optional); + } + + /** + * Get the usage value for this argument + * + * @return string + */ + public function usage() + { + $name = $this->_name; + if ($this->_choices) { + $name = implode('|', $this->_choices); + } + $name = '<' . $name . '>'; + if (!$this->isRequired()) { + $name = '[' . $name . ']'; + } + + return $name; + } + + /** + * Check if this argument is a required argument + * + * @return bool + */ + public function isRequired() + { + return (bool)$this->_required; + } + + /** + * Check that $value is a valid choice for this argument. + * + * @param string $value The choice to validate. + * @return bool + * @throws \Cake\Console\Exception\ConsoleException + */ + public function validChoice($value) + { + if (empty($this->_choices)) { + return true; + } + if (!in_array($value, $this->_choices)) { + throw new ConsoleException( + sprintf( + '"%s" is not a valid value for %s. Please use one of "%s"', + $value, + $this->_name, + implode(', ', $this->_choices) + ) + ); + } + + return true; + } + + /** + * Append this arguments XML representation to the passed in SimpleXml object. + * + * @param \SimpleXMLElement $parent The parent element. + * @return \SimpleXMLElement The parent with this argument appended. + */ + public function xml(SimpleXMLElement $parent) + { + $option = $parent->addChild('argument'); + $option->addAttribute('name', $this->_name); + $option->addAttribute('help', $this->_help); + $option->addAttribute('required', (int)$this->isRequired()); + $choices = $option->addChild('choices'); + foreach ($this->_choices as $valid) { + $choices->addChild('choice', $valid); + } + + return $parent; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/ConsoleInputOption.php b/app/vendor/cakephp/cakephp/src/Console/ConsoleInputOption.php new file mode 100644 index 000000000..87d280a97 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/ConsoleInputOption.php @@ -0,0 +1,265 @@ + $value) { + $this->{'_' . $key} = $value; + } + } else { + $this->_name = $name; + $this->_short = $short; + $this->_help = $help; + $this->_boolean = $boolean; + $this->_default = $default; + $this->_choices = $choices; + $this->_multiple = $multiple; + } + if (strlen($this->_short) > 1) { + throw new ConsoleException( + sprintf('Short option "%s" is invalid, short options must be one letter.', $this->_short) + ); + } + } + + /** + * Get the value of the name attribute. + * + * @return string Value of this->_name. + */ + public function name() + { + return $this->_name; + } + + /** + * Get the value of the short attribute. + * + * @return string Value of this->_short. + */ + public function short() + { + return $this->_short; + } + + /** + * Generate the help for this this option. + * + * @param int $width The width to make the name of the option. + * @return string + */ + public function help($width = 0) + { + $default = $short = ''; + if ($this->_default && $this->_default !== true) { + $default = sprintf(' (default: %s)', $this->_default); + } + if ($this->_choices) { + $default .= sprintf(' (choices: %s)', implode('|', $this->_choices)); + } + if (strlen($this->_short) > 0) { + $short = ', -' . $this->_short; + } + $name = sprintf('--%s%s', $this->_name, $short); + if (strlen($name) < $width) { + $name = str_pad($name, $width, ' '); + } + + return sprintf('%s%s%s', $name, $this->_help, $default); + } + + /** + * Get the usage value for this option + * + * @return string + */ + public function usage() + { + $name = (strlen($this->_short) > 0) ? ('-' . $this->_short) : ('--' . $this->_name); + $default = ''; + if (strlen($this->_default) > 0 && $this->_default !== true) { + $default = ' ' . $this->_default; + } + if ($this->_choices) { + $default = ' ' . implode('|', $this->_choices); + } + + return sprintf('[%s%s]', $name, $default); + } + + /** + * Get the default value for this option + * + * @return mixed + */ + public function defaultValue() + { + return $this->_default; + } + + /** + * Check if this option is a boolean option + * + * @return bool + */ + public function isBoolean() + { + return (bool)$this->_boolean; + } + + /** + * Check if this option accepts multiple values. + * + * @return bool + */ + public function acceptsMultiple() + { + return (bool)$this->_multiple; + } + + /** + * Check that a value is a valid choice for this option. + * + * @param string $value The choice to validate. + * @return bool + * @throws \Cake\Console\Exception\ConsoleException + */ + public function validChoice($value) + { + if (empty($this->_choices)) { + return true; + } + if (!in_array($value, $this->_choices)) { + throw new ConsoleException( + sprintf( + '"%s" is not a valid value for --%s. Please use one of "%s"', + $value, + $this->_name, + implode(', ', $this->_choices) + ) + ); + } + + return true; + } + + /** + * Append the option's xml into the parent. + * + * @param \SimpleXMLElement $parent The parent element. + * @return \SimpleXMLElement The parent with this option appended. + */ + public function xml(SimpleXMLElement $parent) + { + $option = $parent->addChild('option'); + $option->addAttribute('name', '--' . $this->_name); + $short = ''; + if (strlen($this->_short) > 0) { + $short = '-' . $this->_short; + } + $option->addAttribute('short', $short); + $option->addAttribute('help', $this->_help); + $option->addAttribute('boolean', (int)$this->_boolean); + $option->addChild('default', $this->_default); + $choices = $option->addChild('choices'); + foreach ($this->_choices as $valid) { + $choices->addChild('choice', $valid); + } + + return $parent; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/ConsoleInputSubcommand.php b/app/vendor/cakephp/cakephp/src/Console/ConsoleInputSubcommand.php new file mode 100644 index 000000000..1330098c3 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/ConsoleInputSubcommand.php @@ -0,0 +1,140 @@ + $value) { + $this->{'_' . $key} = $value; + } + } else { + $this->_name = $name; + $this->_help = $help; + $this->_parser = $parser; + } + if (is_array($this->_parser)) { + $this->_parser['command'] = $this->_name; + $this->_parser = ConsoleOptionParser::buildFromArray($this->_parser); + } + } + + /** + * Get the value of the name attribute. + * + * @return string Value of this->_name. + */ + public function name() + { + return $this->_name; + } + + /** + * Get the raw help string for this command + * + * @return string + */ + public function getRawHelp() + { + return $this->_help; + } + + /** + * Generate the help for this this subcommand. + * + * @param int $width The width to make the name of the subcommand. + * @return string + */ + public function help($width = 0) + { + $name = $this->_name; + if (strlen($name) < $width) { + $name = str_pad($name, $width, ' '); + } + + return $name . $this->_help; + } + + /** + * Get the usage value for this option + * + * @return \Cake\Console\ConsoleOptionParser|bool Either false or a ConsoleOptionParser + */ + public function parser() + { + if ($this->_parser instanceof ConsoleOptionParser) { + return $this->_parser; + } + + return false; + } + + /** + * Append this subcommand to the Parent element + * + * @param \SimpleXMLElement $parent The parent element. + * @return \SimpleXMLElement The parent with this subcommand appended. + */ + public function xml(SimpleXMLElement $parent) + { + $command = $parent->addChild('command'); + $command->addAttribute('name', $this->_name); + $command->addAttribute('help', $this->_help); + + return $parent; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/ConsoleIo.php b/app/vendor/cakephp/cakephp/src/Console/ConsoleIo.php new file mode 100644 index 000000000..65c5eabe3 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/ConsoleIo.php @@ -0,0 +1,592 @@ +_out = $out ?: new ConsoleOutput('php://stdout'); + $this->_err = $err ?: new ConsoleOutput('php://stderr'); + $this->_in = $in ?: new ConsoleInput('php://stdin'); + $this->_helpers = $helpers ?: new HelperRegistry(); + $this->_helpers->setIo($this); + } + + /** + * Get/set the current output level. + * + * @param null|int $level The current output level. + * @return int The current output level. + */ + public function level($level = null) + { + if ($level !== null) { + $this->_level = $level; + } + + return $this->_level; + } + + /** + * Output at the verbose level. + * + * @param string|array $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return int|bool The number of bytes returned from writing to stdout. + */ + public function verbose($message, $newlines = 1) + { + return $this->out($message, $newlines, self::VERBOSE); + } + + /** + * Output at all levels. + * + * @param string|array $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return int|bool The number of bytes returned from writing to stdout. + */ + public function quiet($message, $newlines = 1) + { + return $this->out($message, $newlines, self::QUIET); + } + + /** + * Outputs a single or multiple messages to stdout. If no parameters + * are passed outputs just a newline. + * + * ### Output levels + * + * There are 3 built-in output level. Shell::QUIET, Shell::NORMAL, Shell::VERBOSE. + * The verbose and quiet output levels, map to the `verbose` and `quiet` output switches + * present in most shells. Using Shell::QUIET for a message means it will always display. + * While using Shell::VERBOSE means it will only display when verbose output is toggled. + * + * @param string|array $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @param int $level The message's output level, see above. + * @return int|bool The number of bytes returned from writing to stdout. + */ + public function out($message = '', $newlines = 1, $level = ConsoleIo::NORMAL) + { + if ($level <= $this->_level) { + $this->_lastWritten = (int)$this->_out->write($message, $newlines); + + return $this->_lastWritten; + } + + return true; + } + + /** + * Convenience method for out() that wraps message between tag + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @param int $level The message's output level, see above. + * @return int|bool The number of bytes returned from writing to stdout. + * @see https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::out + */ + public function info($message = null, $newlines = 1, $level = Shell::NORMAL) + { + $messageType = 'info'; + $message = $this->wrapMessageWithType($messageType, $message); + + return $this->out($message, $newlines, $level); + } + + /** + * Convenience method for err() that wraps message between tag + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return int|bool The number of bytes returned from writing to stderr. + * @see https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::err + */ + public function warning($message = null, $newlines = 1) + { + $messageType = 'warning'; + $message = $this->wrapMessageWithType($messageType, $message); + + return $this->err($message, $newlines); + } + + /** + * Convenience method for err() that wraps message between tag + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return int|bool The number of bytes returned from writing to stderr. + * @see https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::err + */ + public function error($message = null, $newlines = 1) + { + $messageType = 'error'; + $message = $this->wrapMessageWithType($messageType, $message); + + return $this->err($message, $newlines); + } + + /** + * Convenience method for out() that wraps message between tag + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @param int $level The message's output level, see above. + * @return int|bool The number of bytes returned from writing to stdout. + * @see https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::out + */ + public function success($message = null, $newlines = 1, $level = Shell::NORMAL) + { + $messageType = 'success'; + $message = $this->wrapMessageWithType($messageType, $message); + + return $this->out($message, $newlines, $level); + } + + /** + * Wraps a message with a given message type, e.g. + * + * @param string $messageType The message type, e.g. "warning". + * @param string|array $message The message to wrap. + * @return array|string The message wrapped with the given message type. + */ + protected function wrapMessageWithType($messageType, $message) + { + if (is_array($message)) { + foreach ($message as $k => $v) { + $message[$k] = "<{$messageType}>{$v}"; + } + } else { + $message = "<{$messageType}>{$message}"; + } + + return $message; + } + + /** + * Overwrite some already output text. + * + * Useful for building progress bars, or when you want to replace + * text already output to the screen with new text. + * + * **Warning** You cannot overwrite text that contains newlines. + * + * @param array|string $message The message to output. + * @param int $newlines Number of newlines to append. + * @param int|null $size The number of bytes to overwrite. Defaults to the + * length of the last message output. + * @return void + */ + public function overwrite($message, $newlines = 1, $size = null) + { + $size = $size ?: $this->_lastWritten; + + // Output backspaces. + $this->out(str_repeat("\x08", $size), 0); + + $newBytes = $this->out($message, 0); + + // Fill any remaining bytes with spaces. + $fill = $size - $newBytes; + if ($fill > 0) { + $this->out(str_repeat(' ', $fill), 0); + } + if ($newlines) { + $this->out($this->nl($newlines), 0); + } + + // Store length of content + fill so if the new content + // is shorter than the old content the next overwrite + // will work. + if ($fill > 0) { + $this->_lastWritten = $newBytes + $fill; + } + } + + /** + * Outputs a single or multiple error messages to stderr. If no parameters + * are passed outputs just a newline. + * + * @param string|array $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return int|bool The number of bytes returned from writing to stderr. + */ + public function err($message = '', $newlines = 1) + { + return $this->_err->write($message, $newlines); + } + + /** + * Returns a single or multiple linefeeds sequences. + * + * @param int $multiplier Number of times the linefeed sequence should be repeated + * @return string + */ + public function nl($multiplier = 1) + { + return str_repeat(ConsoleOutput::LF, $multiplier); + } + + /** + * Outputs a series of minus characters to the standard output, acts as a visual separator. + * + * @param int $newlines Number of newlines to pre- and append + * @param int $width Width of the line, defaults to 79 + * @return void + */ + public function hr($newlines = 0, $width = 79) + { + $this->out(null, $newlines); + $this->out(str_repeat('-', $width)); + $this->out(null, $newlines); + } + + /** + * Prompts the user for input, and returns it. + * + * @param string $prompt Prompt text. + * @param string|null $default Default input value. + * @return mixed Either the default value, or the user-provided input. + */ + public function ask($prompt, $default = null) + { + return $this->_getInput($prompt, null, $default); + } + + /** + * Change the output mode of the stdout stream + * + * @param int $mode The output mode. + * @return void + * @see \Cake\Console\ConsoleOutput::setOutputAs() + */ + public function setOutputAs($mode) + { + $this->_out->setOutputAs($mode); + } + + /** + * Change the output mode of the stdout stream + * + * @deprecated 3.5.0 Use setOutputAs() instead. + * @param int $mode The output mode. + * @return void + * @see \Cake\Console\ConsoleOutput::outputAs() + */ + public function outputAs($mode) + { + deprecationWarning('ConsoleIo::outputAs() is deprecated. Use ConsoleIo::setOutputAs() instead.'); + $this->_out->setOutputAs($mode); + } + + /** + * Add a new output style or get defined styles. + * + * @param string|null $style The style to get or create. + * @param array|bool|null $definition The array definition of the style to change or create a style + * or false to remove a style. + * @return mixed If you are getting styles, the style or null will be returned. If you are creating/modifying + * styles true will be returned. + * @see \Cake\Console\ConsoleOutput::styles() + */ + public function styles($style = null, $definition = null) + { + $this->_out->styles($style, $definition); + } + + /** + * Prompts the user for input based on a list of options, and returns it. + * + * @param string $prompt Prompt text. + * @param string|array $options Array or string of options. + * @param string|null $default Default input value. + * @return mixed Either the default value, or the user-provided input. + */ + public function askChoice($prompt, $options, $default = null) + { + if ($options && is_string($options)) { + if (strpos($options, ',')) { + $options = explode(',', $options); + } elseif (strpos($options, '/')) { + $options = explode('/', $options); + } else { + $options = [$options]; + } + } + + $printOptions = '(' . implode('/', $options) . ')'; + $options = array_merge( + array_map('strtolower', $options), + array_map('strtoupper', $options), + $options + ); + $in = ''; + while ($in === '' || !in_array($in, $options)) { + $in = $this->_getInput($prompt, $printOptions, $default); + } + + return $in; + } + + /** + * Prompts the user for input, and returns it. + * + * @param string $prompt Prompt text. + * @param string|null $options String of options. Pass null to omit. + * @param string|null $default Default input value. Pass null to omit. + * @return string Either the default value, or the user-provided input. + */ + protected function _getInput($prompt, $options, $default) + { + $optionsText = ''; + if (isset($options)) { + $optionsText = " $options "; + } + + $defaultText = ''; + if ($default !== null) { + $defaultText = "[$default] "; + } + $this->_out->write('' . $prompt . "$optionsText\n$defaultText> ", 0); + $result = $this->_in->read(); + + $result = trim($result); + if ($default !== null && ($result === '' || $result === null)) { + return $default; + } + + return $result; + } + + /** + * Connects or disconnects the loggers to the console output. + * + * Used to enable or disable logging stream output to stdout and stderr + * If you don't wish all log output in stdout or stderr + * through Cake's Log class, call this function with `$enable=false`. + * + * @param int|bool $enable Use a boolean to enable/toggle all logging. Use + * one of the verbosity constants (self::VERBOSE, self::QUIET, self::NORMAL) + * to control logging levels. VERBOSE enables debug logs, NORMAL does not include debug logs, + * QUIET disables notice, info and debug logs. + * @return void + */ + public function setLoggers($enable) + { + Log::drop('stdout'); + Log::drop('stderr'); + if ($enable === false) { + return; + } + $outLevels = ['notice', 'info']; + if ($enable === static::VERBOSE || $enable === true) { + $outLevels[] = 'debug'; + } + if ($enable !== static::QUIET) { + $stdout = new ConsoleLog([ + 'types' => $outLevels, + 'stream' => $this->_out + ]); + Log::setConfig('stdout', ['engine' => $stdout]); + } + $stderr = new ConsoleLog([ + 'types' => ['emergency', 'alert', 'critical', 'error', 'warning'], + 'stream' => $this->_err, + ]); + Log::setConfig('stderr', ['engine' => $stderr]); + } + + /** + * Render a Console Helper + * + * Create and render the output for a helper object. If the helper + * object has not already been loaded, it will be loaded and constructed. + * + * @param string $name The name of the helper to render + * @param array $settings Configuration data for the helper. + * @return \Cake\Console\Helper The created helper instance. + */ + public function helper($name, array $settings = []) + { + $name = ucfirst($name); + + return $this->_helpers->load($name, $settings); + } + + /** + * Create a file at the given path. + * + * This method will prompt the user if a file will be overwritten. + * Setting `forceOverwrite` to true will suppress this behavior + * and always overwrite the file. + * + * If the user replies `a` subsequent `forceOverwrite` parameters will + * be coerced to true and all files will be overwritten. + * + * @param string $path The path to create the file at. + * @param string $contents The contents to put into the file. + * @param bool $forceOverwrite Whether or not the file should be overwritten. + * If true, no question will be asked about whether or not to overwrite existing files. + * @return bool Success. + * @throws \Cake\Console\Exception\StopException When `q` is given as an answer + * to whether or not a file should be overwritten. + */ + public function createFile($path, $contents, $forceOverwrite = false) + { + $path = str_replace( + DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR, + DIRECTORY_SEPARATOR, + $path + ); + + $this->out(); + $forceOverwrite = $forceOverwrite || $this->forceOverwrite; + + if (file_exists($path) && $forceOverwrite === false) { + $this->warning("File `{$path}` exists"); + $key = $this->askChoice('Do you want to overwrite?', ['y', 'n', 'a', 'q'], 'n'); + $key = strtolower($key); + + if ($key === 'q') { + $this->error('Quitting.', 2); + throw new StopException('Not creating file. Quitting.'); + } + if ($key === 'a') { + $this->forceOverwrite = true; + $key = 'y'; + } + if ($key !== 'y') { + $this->out("Skip `{$path}`", 2); + + return false; + } + } else { + $this->out("Creating file {$path}"); + } + + try { + $file = new SplFileObject($path, 'w'); + } catch (RuntimeException $e) { + $this->error("Could not write to `{$path}`. Permission denied.", 2); + + return false; + } + + $file->rewind(); + if ($file->fwrite($contents) > 0) { + $this->out("Wrote `{$path}`"); + + return true; + } + $this->error("Could not write to `{$path}`.", 2); + + return false; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/ConsoleOptionParser.php b/app/vendor/cakephp/cakephp/src/Console/ConsoleOptionParser.php new file mode 100644 index 000000000..52f479529 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/ConsoleOptionParser.php @@ -0,0 +1,1120 @@ +addOption()` + * you can define new options. The name of the option is used as its long form, and you + * can supply an additional short form, with the `short` option. Short options should + * only be one letter long. Using more than one letter for a short option will raise an exception. + * + * Calling options can be done using syntax similar to most *nix command line tools. Long options + * cane either include an `=` or leave it out. + * + * `cake myshell command --connection default --name=something` + * + * Short options can be defined singly or in groups. + * + * `cake myshell command -cn` + * + * Short options can be combined into groups as seen above. Each letter in a group + * will be treated as a separate option. The previous example is equivalent to: + * + * `cake myshell command -c -n` + * + * Short options can also accept values: + * + * `cake myshell command -c default` + * + * ### Positional arguments + * + * If no positional arguments are defined, all of them will be parsed. If you define positional + * arguments any arguments greater than those defined will cause exceptions. Additionally you can + * declare arguments as optional, by setting the required param to false. + * + * ``` + * $parser->addArgument('model', ['required' => false]); + * ``` + * + * ### Providing Help text + * + * By providing help text for your positional arguments and named arguments, the ConsoleOptionParser + * can generate a help display for you. You can view the help for shells by using the `--help` or `-h` switch. + */ +class ConsoleOptionParser +{ + + /** + * Description text - displays before options when help is generated + * + * @see \Cake\Console\ConsoleOptionParser::description() + * @var string + */ + protected $_description; + + /** + * Epilog text - displays after options when help is generated + * + * @see \Cake\Console\ConsoleOptionParser::epilog() + * @var string + */ + protected $_epilog; + + /** + * Option definitions. + * + * @see \Cake\Console\ConsoleOptionParser::addOption() + * @var \Cake\Console\ConsoleInputOption[] + */ + protected $_options = []; + + /** + * Map of short -> long options, generated when using addOption() + * + * @var array + */ + protected $_shortOptions = []; + + /** + * Positional argument definitions. + * + * @see \Cake\Console\ConsoleOptionParser::addArgument() + * @var \Cake\Console\ConsoleInputArgument[] + */ + protected $_args = []; + + /** + * Subcommands for this Shell. + * + * @see \Cake\Console\ConsoleOptionParser::addSubcommand() + * @var \Cake\Console\ConsoleInputSubcommand[] + */ + protected $_subcommands = []; + + /** + * Subcommand sorting option + * + * @var bool + */ + protected $_subcommandSort = true; + + /** + * Command name. + * + * @var string + */ + protected $_command = ''; + + /** + * Array of args (argv). + * + * @var array + */ + protected $_tokens = []; + + /** + * Root alias used in help output + * + * @see \Cake\Console\HelpFormatter::setAlias() + * @var string + */ + protected $rootName = 'cake'; + + /** + * Construct an OptionParser so you can define its behavior + * + * @param string|null $command The command name this parser is for. The command name is used for generating help. + * @param bool $defaultOptions Whether you want the verbose and quiet options set. Setting + * this to false will prevent the addition of `--verbose` & `--quiet` options. + */ + public function __construct($command = null, $defaultOptions = true) + { + $this->setCommand($command); + + $this->addOption('help', [ + 'short' => 'h', + 'help' => 'Display this help.', + 'boolean' => true + ]); + + if ($defaultOptions) { + $this->addOption('verbose', [ + 'short' => 'v', + 'help' => 'Enable verbose output.', + 'boolean' => true + ])->addOption('quiet', [ + 'short' => 'q', + 'help' => 'Enable quiet output.', + 'boolean' => true + ]); + } + } + + /** + * Static factory method for creating new OptionParsers so you can chain methods off of them. + * + * @param string|null $command The command name this parser is for. The command name is used for generating help. + * @param bool $defaultOptions Whether you want the verbose and quiet options set. + * @return static + */ + public static function create($command, $defaultOptions = true) + { + return new static($command, $defaultOptions); + } + + /** + * Build a parser from an array. Uses an array like + * + * ``` + * $spec = [ + * 'description' => 'text', + * 'epilog' => 'text', + * 'arguments' => [ + * // list of arguments compatible with addArguments. + * ], + * 'options' => [ + * // list of options compatible with addOptions + * ], + * 'subcommands' => [ + * // list of subcommands to add. + * ] + * ]; + * ``` + * + * @param array $spec The spec to build the OptionParser with. + * @param bool $defaultOptions Whether you want the verbose and quiet options set. + * @return static + */ + public static function buildFromArray($spec, $defaultOptions = true) + { + $parser = new static($spec['command'], $defaultOptions); + if (!empty($spec['arguments'])) { + $parser->addArguments($spec['arguments']); + } + if (!empty($spec['options'])) { + $parser->addOptions($spec['options']); + } + if (!empty($spec['subcommands'])) { + $parser->addSubcommands($spec['subcommands']); + } + if (!empty($spec['description'])) { + $parser->setDescription($spec['description']); + } + if (!empty($spec['epilog'])) { + $parser->setEpilog($spec['epilog']); + } + + return $parser; + } + + /** + * Returns an array representation of this parser. + * + * @return array + */ + public function toArray() + { + $result = [ + 'command' => $this->_command, + 'arguments' => $this->_args, + 'options' => $this->_options, + 'subcommands' => $this->_subcommands, + 'description' => $this->_description, + 'epilog' => $this->_epilog + ]; + + return $result; + } + + /** + * Get or set the command name for shell/task. + * + * @param array|\Cake\Console\ConsoleOptionParser $spec ConsoleOptionParser or spec to merge with. + * @return $this + */ + public function merge($spec) + { + if ($spec instanceof ConsoleOptionParser) { + $spec = $spec->toArray(); + } + if (!empty($spec['arguments'])) { + $this->addArguments($spec['arguments']); + } + if (!empty($spec['options'])) { + $this->addOptions($spec['options']); + } + if (!empty($spec['subcommands'])) { + $this->addSubcommands($spec['subcommands']); + } + if (!empty($spec['description'])) { + $this->setDescription($spec['description']); + } + if (!empty($spec['epilog'])) { + $this->setEpilog($spec['epilog']); + } + + return $this; + } + + /** + * Sets the command name for shell/task. + * + * @param string $text The text to set. + * @return $this + */ + public function setCommand($text) + { + $this->_command = Inflector::underscore($text); + + return $this; + } + + /** + * Gets the command name for shell/task. + * + * @return string The value of the command. + */ + public function getCommand() + { + return $this->_command; + } + + /** + * Gets or sets the command name for shell/task. + * + * @deprecated 3.4.0 Use setCommand()/getCommand() instead. + * @param string|null $text The text to set, or null if you want to read + * @return string|$this If reading, the value of the command. If setting $this will be returned. + */ + public function command($text = null) + { + deprecationWarning( + 'ConsoleOptionParser::command() is deprecated. ' . + 'Use ConsoleOptionParser::setCommand()/getCommand() instead.' + ); + if ($text !== null) { + return $this->setCommand($text); + } + + return $this->getCommand(); + } + + /** + * Sets the description text for shell/task. + * + * @param string|array $text The text to set. If an array the + * text will be imploded with "\n". + * @return $this + */ + public function setDescription($text) + { + if (is_array($text)) { + $text = implode("\n", $text); + } + $this->_description = $text; + + return $this; + } + + /** + * Gets the description text for shell/task. + * + * @return string The value of the description + */ + public function getDescription() + { + return $this->_description; + } + + /** + * Get or set the description text for shell/task. + * + * @deprecated 3.4.0 Use setDescription()/getDescription() instead. + * @param string|array|null $text The text to set, or null if you want to read. If an array the + * text will be imploded with "\n". + * @return string|$this If reading, the value of the description. If setting $this will be returned. + */ + public function description($text = null) + { + deprecationWarning( + 'ConsoleOptionParser::description() is deprecated. ' . + 'Use ConsoleOptionParser::setDescription()/getDescription() instead.' + ); + if ($text !== null) { + return $this->setDescription($text); + } + + return $this->getDescription(); + } + + /** + * Sets an epilog to the parser. The epilog is added to the end of + * the options and arguments listing when help is generated. + * + * @param string|array $text The text to set. If an array the text will + * be imploded with "\n". + * @return $this + */ + public function setEpilog($text) + { + if (is_array($text)) { + $text = implode("\n", $text); + } + $this->_epilog = $text; + + return $this; + } + + /** + * Gets the epilog. + * + * @return string The value of the epilog. + */ + public function getEpilog() + { + return $this->_epilog; + } + + /** + * Gets or sets an epilog to the parser. The epilog is added to the end of + * the options and arguments listing when help is generated. + * + * @deprecated 3.4.0 Use setEpilog()/getEpilog() instead. + * @param string|array|null $text Text when setting or null when reading. If an array the text will + * be imploded with "\n". + * @return string|$this If reading, the value of the epilog. If setting $this will be returned. + */ + public function epilog($text = null) + { + deprecationWarning( + 'ConsoleOptionParser::epliog() is deprecated. ' . + 'Use ConsoleOptionParser::setEpilog()/getEpilog() instead.' + ); + if ($text !== null) { + return $this->setEpilog($text); + } + + return $this->getEpilog(); + } + + /** + * Enables sorting of subcommands + * + * @param bool $value Whether or not to sort subcommands + * @return $this + */ + public function enableSubcommandSort($value = true) + { + $this->_subcommandSort = (bool)$value; + + return $this; + } + + /** + * Checks whether or not sorting is enabled for subcommands. + * + * @return bool + */ + public function isSubcommandSortEnabled() + { + return $this->_subcommandSort; + } + + /** + * Add an option to the option parser. Options allow you to define optional or required + * parameters for your console application. Options are defined by the parameters they use. + * + * ### Options + * + * - `short` - The single letter variant for this option, leave undefined for none. + * - `help` - Help text for this option. Used when generating help for the option. + * - `default` - The default value for this option. Defaults are added into the parsed params when the + * attached option is not provided or has no value. Using default and boolean together will not work. + * are added into the parsed parameters when the option is undefined. Defaults to null. + * - `boolean` - The option uses no value, it's just a boolean switch. Defaults to false. + * If an option is defined as boolean, it will always be added to the parsed params. If no present + * it will be false, if present it will be true. + * - `multiple` - The option can be provided multiple times. The parsed option + * will be an array of values when this option is enabled. + * - `choices` A list of valid choices for this option. If left empty all values are valid.. + * An exception will be raised when parse() encounters an invalid value. + * + * @param \Cake\Console\ConsoleInputOption|string $name The long name you want to the value to be parsed out as when options are parsed. + * Will also accept an instance of ConsoleInputOption + * @param array $options An array of parameters that define the behavior of the option + * @return $this + */ + public function addOption($name, array $options = []) + { + if ($name instanceof ConsoleInputOption) { + $option = $name; + $name = $option->name(); + } else { + $defaults = [ + 'name' => $name, + 'short' => null, + 'help' => '', + 'default' => null, + 'boolean' => false, + 'choices' => [] + ]; + $options += $defaults; + $option = new ConsoleInputOption($options); + } + $this->_options[$name] = $option; + asort($this->_options); + if ($option->short() !== null) { + $this->_shortOptions[$option->short()] = $name; + asort($this->_shortOptions); + } + + return $this; + } + + /** + * Remove an option from the option parser. + * + * @param string $name The option name to remove. + * @return $this + */ + public function removeOption($name) + { + unset($this->_options[$name]); + + return $this; + } + + /** + * Add a positional argument to the option parser. + * + * ### Params + * + * - `help` The help text to display for this argument. + * - `required` Whether this parameter is required. + * - `index` The index for the arg, if left undefined the argument will be put + * onto the end of the arguments. If you define the same index twice the first + * option will be overwritten. + * - `choices` A list of valid choices for this argument. If left empty all values are valid.. + * An exception will be raised when parse() encounters an invalid value. + * + * @param \Cake\Console\ConsoleInputArgument|string $name The name of the argument. + * Will also accept an instance of ConsoleInputArgument. + * @param array $params Parameters for the argument, see above. + * @return $this + */ + public function addArgument($name, array $params = []) + { + if ($name instanceof ConsoleInputArgument) { + $arg = $name; + $index = count($this->_args); + } else { + $defaults = [ + 'name' => $name, + 'help' => '', + 'index' => count($this->_args), + 'required' => false, + 'choices' => [] + ]; + $options = $params + $defaults; + $index = $options['index']; + unset($options['index']); + $arg = new ConsoleInputArgument($options); + } + foreach ($this->_args as $k => $a) { + if ($a->isEqualTo($arg)) { + return $this; + } + if (!empty($options['required']) && !$a->isRequired()) { + throw new LogicException('A required argument cannot follow an optional one'); + } + } + $this->_args[$index] = $arg; + ksort($this->_args); + + return $this; + } + + /** + * Add multiple arguments at once. Take an array of argument definitions. + * The keys are used as the argument names, and the values as params for the argument. + * + * @param array $args Array of arguments to add. + * @see \Cake\Console\ConsoleOptionParser::addArgument() + * @return $this + */ + public function addArguments(array $args) + { + foreach ($args as $name => $params) { + if ($params instanceof ConsoleInputArgument) { + $name = $params; + $params = []; + } + $this->addArgument($name, $params); + } + + return $this; + } + + /** + * Add multiple options at once. Takes an array of option definitions. + * The keys are used as option names, and the values as params for the option. + * + * @param array $options Array of options to add. + * @see \Cake\Console\ConsoleOptionParser::addOption() + * @return $this + */ + public function addOptions(array $options) + { + foreach ($options as $name => $params) { + if ($params instanceof ConsoleInputOption) { + $name = $params; + $params = []; + } + $this->addOption($name, $params); + } + + return $this; + } + + /** + * Append a subcommand to the subcommand list. + * Subcommands are usually methods on your Shell, but can also be used to document Tasks. + * + * ### Options + * + * - `help` - Help text for the subcommand. + * - `parser` - A ConsoleOptionParser for the subcommand. This allows you to create method + * specific option parsers. When help is generated for a subcommand, if a parser is present + * it will be used. + * + * @param \Cake\Console\ConsoleInputSubcommand|string $name Name of the subcommand. Will also accept an instance of ConsoleInputSubcommand + * @param array $options Array of params, see above. + * @return $this + */ + public function addSubcommand($name, array $options = []) + { + if ($name instanceof ConsoleInputSubcommand) { + $command = $name; + $name = $command->name(); + } else { + $name = Inflector::underscore($name); + $defaults = [ + 'name' => $name, + 'help' => '', + 'parser' => null + ]; + $options += $defaults; + + $command = new ConsoleInputSubcommand($options); + } + $this->_subcommands[$name] = $command; + if ($this->_subcommandSort) { + asort($this->_subcommands); + } + + return $this; + } + + /** + * Remove a subcommand from the option parser. + * + * @param string $name The subcommand name to remove. + * @return $this + */ + public function removeSubcommand($name) + { + unset($this->_subcommands[$name]); + + return $this; + } + + /** + * Add multiple subcommands at once. + * + * @param array $commands Array of subcommands. + * @return $this + */ + public function addSubcommands(array $commands) + { + foreach ($commands as $name => $params) { + if ($params instanceof ConsoleInputSubcommand) { + $name = $params; + $params = []; + } + $this->addSubcommand($name, $params); + } + + return $this; + } + + /** + * Gets the arguments defined in the parser. + * + * @return \Cake\Console\ConsoleInputArgument[] + */ + public function arguments() + { + return $this->_args; + } + + /** + * Get the list of argument names. + * + * @return string[] + */ + public function argumentNames() + { + $out = []; + foreach ($this->_args as $arg) { + $out[] = $arg->name(); + } + + return $out; + } + + /** + * Get the defined options in the parser. + * + * @return \Cake\Console\ConsoleInputOption[] + */ + public function options() + { + return $this->_options; + } + + /** + * Get the array of defined subcommands + * + * @return \Cake\Console\ConsoleInputSubcommand[] + */ + public function subcommands() + { + return $this->_subcommands; + } + + /** + * Parse the argv array into a set of params and args. If $command is not null + * and $command is equal to a subcommand that has a parser, that parser will be used + * to parse the $argv + * + * @param array $argv Array of args (argv) to parse. + * @return array [$params, $args] + * @throws \Cake\Console\Exception\ConsoleException When an invalid parameter is encountered. + */ + public function parse($argv) + { + $command = isset($argv[0]) ? Inflector::underscore($argv[0]) : null; + if (isset($this->_subcommands[$command])) { + array_shift($argv); + } + if (isset($this->_subcommands[$command]) && $this->_subcommands[$command]->parser()) { + return $this->_subcommands[$command]->parser()->parse($argv); + } + $params = $args = []; + $this->_tokens = $argv; + while (($token = array_shift($this->_tokens)) !== null) { + if (isset($this->_subcommands[$token])) { + continue; + } + if (substr($token, 0, 2) === '--') { + $params = $this->_parseLongOption($token, $params); + } elseif (substr($token, 0, 1) === '-') { + $params = $this->_parseShortOption($token, $params); + } else { + $args = $this->_parseArg($token, $args); + } + } + foreach ($this->_args as $i => $arg) { + if ($arg->isRequired() && !isset($args[$i]) && empty($params['help'])) { + throw new ConsoleException( + sprintf('Missing required arguments. %s is required.', $arg->name()) + ); + } + } + foreach ($this->_options as $option) { + $name = $option->name(); + $isBoolean = $option->isBoolean(); + $default = $option->defaultValue(); + + if ($default !== null && !isset($params[$name]) && !$isBoolean) { + $params[$name] = $default; + } + if ($isBoolean && !isset($params[$name])) { + $params[$name] = false; + } + } + + return [$params, $args]; + } + + /** + * Gets formatted help for this parser object. + * + * Generates help text based on the description, options, arguments, subcommands and epilog + * in the parser. + * + * @param string|null $subcommand If present and a valid subcommand that has a linked parser. + * That subcommands help will be shown instead. + * @param string $format Define the output format, can be text or xml + * @param int $width The width to format user content to. Defaults to 72 + * @return string Generated help. + */ + public function help($subcommand = null, $format = 'text', $width = 72) + { + if ($subcommand === null) { + $formatter = new HelpFormatter($this); + $formatter->setAlias($this->rootName); + + if ($format === 'text') { + return $formatter->text($width); + } + if ($format === 'xml') { + return (string)$formatter->xml(); + } + } + + if (isset($this->_subcommands[$subcommand])) { + $command = $this->_subcommands[$subcommand]; + $subparser = $command->parser(); + + // Generate a parser as the subcommand didn't define one. + if (!($subparser instanceof self)) { + // $subparser = clone $this; + $subparser = new self($subcommand); + $subparser + ->setDescription($command->getRawHelp()) + ->addOptions($this->options()) + ->addArguments($this->arguments()); + } + if (strlen($subparser->getDescription()) === 0) { + $subparser->setDescription($command->getRawHelp()); + } + $subparser->setCommand($this->getCommand() . ' ' . $subcommand); + $subparser->setRootName($this->rootName); + + return $subparser->help(null, $format, $width); + } + + return $this->getCommandError($subcommand); + } + + /** + * Set the alias used in the HelpFormatter + * + * @param string $alias The alias + * @return void + * @deprecated 3.5.0 Use setRootName() instead. + */ + public function setHelpAlias($alias) + { + deprecationWarning( + 'ConsoleOptionParser::setHelpAlias() is deprecated. ' . + 'Use ConsoleOptionParser::setRootName() instead.' + ); + $this->rootName = $alias; + } + + /** + * Set the root name used in the HelpFormatter + * + * @param string $name The root command name + * @return $this + */ + public function setRootName($name) + { + $this->rootName = (string)$name; + + return $this; + } + + /** + * Get the message output in the console stating that the command can not be found and tries to guess what the user + * wanted to say. Output a list of available subcommands as well. + * + * @param string $command Unknown command name trying to be dispatched. + * @return string The message to be displayed in the console. + */ + protected function getCommandError($command) + { + $rootCommand = $this->getCommand(); + $subcommands = array_keys((array)$this->subcommands()); + $bestGuess = $this->findClosestItem($command, $subcommands); + + $out = [ + sprintf( + 'Unable to find the `%s %s` subcommand. See `bin/%s %s --help`.', + $rootCommand, + $command, + $this->rootName, + $rootCommand + ), + '' + ]; + + if ($bestGuess !== null) { + $out[] = sprintf('Did you mean : `%s %s` ?', $rootCommand, $bestGuess); + $out[] = ''; + } + $out[] = sprintf('Available subcommands for the `%s` command are : ', $rootCommand); + $out[] = ''; + foreach ($subcommands as $subcommand) { + $out[] = ' - ' . $subcommand; + } + + return implode("\n", $out); + } + + /** + * Get the message output in the console stating that the option can not be found and tries to guess what the user + * wanted to say. Output a list of available options as well. + * + * @param string $option Unknown option name trying to be used. + * @return string The message to be displayed in the console. + */ + protected function getOptionError($option) + { + $availableOptions = array_keys($this->_options); + $bestGuess = $this->findClosestItem($option, $availableOptions); + $out = [ + sprintf('Unknown option `%s`.', $option), + '' + ]; + + if ($bestGuess !== null) { + $out[] = sprintf('Did you mean `%s` ?', $bestGuess); + $out[] = ''; + } + + $out[] = 'Available options are :'; + $out[] = ''; + foreach ($availableOptions as $availableOption) { + $out[] = ' - ' . $availableOption; + } + + return implode("\n", $out); + } + + /** + * Get the message output in the console stating that the short option can not be found. Output a list of available + * short options and what option they refer to as well. + * + * @param string $option Unknown short option name trying to be used. + * @return string The message to be displayed in the console. + */ + protected function getShortOptionError($option) + { + $out = [sprintf('Unknown short option `%s`', $option)]; + $out[] = ''; + $out[] = 'Available short options are :'; + $out[] = ''; + + foreach ($this->_shortOptions as $short => $long) { + $out[] = sprintf(' - `%s` (short for `--%s`)', $short, $long); + } + + return implode("\n", $out); + } + + /** + * Tries to guess the item name the user originally wanted using the some regex pattern and the levenshtein + * algorithm. + * + * @param string $needle Unknown item (either a subcommand name or an option for instance) trying to be used. + * @param array $haystack List of items available for the type $needle belongs to. + * @return string|null The closest name to the item submitted by the user. + */ + protected function findClosestItem($needle, $haystack) + { + $bestGuess = null; + foreach ($haystack as $item) { + if (preg_match('/^' . $needle . '/', $item)) { + return $item; + } + } + + foreach ($haystack as $item) { + if (preg_match('/' . $needle . '/', $item)) { + return $item; + } + + $score = levenshtein($needle, $item); + + if (!isset($bestScore) || $score < $bestScore) { + $bestScore = $score; + $bestGuess = $item; + } + } + + return $bestGuess; + } + + /** + * Parse the value for a long option out of $this->_tokens. Will handle + * options with an `=` in them. + * + * @param string $option The option to parse. + * @param array $params The params to append the parsed value into + * @return array Params with $option added in. + */ + protected function _parseLongOption($option, $params) + { + $name = substr($option, 2); + if (strpos($name, '=') !== false) { + list($name, $value) = explode('=', $name, 2); + array_unshift($this->_tokens, $value); + } + + return $this->_parseOption($name, $params); + } + + /** + * Parse the value for a short option out of $this->_tokens + * If the $option is a combination of multiple shortcuts like -otf + * they will be shifted onto the token stack and parsed individually. + * + * @param string $option The option to parse. + * @param array $params The params to append the parsed value into + * @return array Params with $option added in. + * @throws \Cake\Console\Exception\ConsoleException When unknown short options are encountered. + */ + protected function _parseShortOption($option, $params) + { + $key = substr($option, 1); + if (strlen($key) > 1) { + $flags = str_split($key); + $key = $flags[0]; + for ($i = 1, $len = count($flags); $i < $len; $i++) { + array_unshift($this->_tokens, '-' . $flags[$i]); + } + } + if (!isset($this->_shortOptions[$key])) { + throw new ConsoleException($this->getShortOptionError($key)); + } + $name = $this->_shortOptions[$key]; + + return $this->_parseOption($name, $params); + } + + /** + * Parse an option by its name index. + * + * @param string $name The name to parse. + * @param array $params The params to append the parsed value into + * @return array Params with $option added in. + * @throws \Cake\Console\Exception\ConsoleException + */ + protected function _parseOption($name, $params) + { + if (!isset($this->_options[$name])) { + throw new ConsoleException($this->getOptionError($name)); + } + $option = $this->_options[$name]; + $isBoolean = $option->isBoolean(); + $nextValue = $this->_nextToken(); + $emptyNextValue = (empty($nextValue) && $nextValue !== '0'); + if (!$isBoolean && !$emptyNextValue && !$this->_optionExists($nextValue)) { + array_shift($this->_tokens); + $value = $nextValue; + } elseif ($isBoolean) { + $value = true; + } else { + $value = $option->defaultValue(); + } + if ($option->validChoice($value)) { + if ($option->acceptsMultiple()) { + $params[$name][] = $value; + } else { + $params[$name] = $value; + } + + return $params; + } + + return []; + } + + /** + * Check to see if $name has an option (short/long) defined for it. + * + * @param string $name The name of the option. + * @return bool + */ + protected function _optionExists($name) + { + if (substr($name, 0, 2) === '--') { + return isset($this->_options[substr($name, 2)]); + } + if ($name{0} === '-' && $name{1} !== '-') { + return isset($this->_shortOptions[$name{1}]); + } + + return false; + } + + /** + * Parse an argument, and ensure that the argument doesn't exceed the number of arguments + * and that the argument is a valid choice. + * + * @param string $argument The argument to append + * @param array $args The array of parsed args to append to. + * @return array Args + * @throws \Cake\Console\Exception\ConsoleException + */ + protected function _parseArg($argument, $args) + { + if (empty($this->_args)) { + $args[] = $argument; + + return $args; + } + $next = count($args); + if (!isset($this->_args[$next])) { + throw new ConsoleException('Too many arguments.'); + } + + if ($this->_args[$next]->validChoice($argument)) { + $args[] = $argument; + + return $args; + } + } + + /** + * Find the next token in the argv set. + * + * @return string next token or '' + */ + protected function _nextToken() + { + return isset($this->_tokens[0]) ? $this->_tokens[0] : ''; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/ConsoleOutput.php b/app/vendor/cakephp/cakephp/src/Console/ConsoleOutput.php new file mode 100644 index 000000000..d16ba8048 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/ConsoleOutput.php @@ -0,0 +1,361 @@ +out('Overwrite: foo.php was overwritten.'); + * ``` + * + * This would create orange 'Overwrite:' text, while the rest of the text would remain the normal color. + * See ConsoleOutput::styles() to learn more about defining your own styles. Nested styles are not supported + * at this time. + */ +class ConsoleOutput +{ + + /** + * Raw output constant - no modification of output text. + * + * @var int + */ + const RAW = 0; + + /** + * Plain output - tags will be stripped. + * + * @var int + */ + const PLAIN = 1; + + /** + * Color output - Convert known tags in to ANSI color escape codes. + * + * @var int + */ + const COLOR = 2; + + /** + * Constant for a newline. + * + * @var string + */ + const LF = PHP_EOL; + + /** + * File handle for output. + * + * @var resource + */ + protected $_output; + + /** + * The current output type. Manipulated with ConsoleOutput::outputAs(); + * + * @var int + */ + protected $_outputAs = self::COLOR; + + /** + * text colors used in colored output. + * + * @var array + */ + protected static $_foregroundColors = [ + 'black' => 30, + 'red' => 31, + 'green' => 32, + 'yellow' => 33, + 'blue' => 34, + 'magenta' => 35, + 'cyan' => 36, + 'white' => 37 + ]; + + /** + * background colors used in colored output. + * + * @var array + */ + protected static $_backgroundColors = [ + 'black' => 40, + 'red' => 41, + 'green' => 42, + 'yellow' => 43, + 'blue' => 44, + 'magenta' => 45, + 'cyan' => 46, + 'white' => 47 + ]; + + /** + * Formatting options for colored output. + * + * @var array + */ + protected static $_options = [ + 'bold' => 1, + 'underline' => 4, + 'blink' => 5, + 'reverse' => 7, + ]; + + /** + * Styles that are available as tags in console output. + * You can modify these styles with ConsoleOutput::styles() + * + * @var array + */ + protected static $_styles = [ + 'emergency' => ['text' => 'red'], + 'alert' => ['text' => 'red'], + 'critical' => ['text' => 'red'], + 'error' => ['text' => 'red'], + 'warning' => ['text' => 'yellow'], + 'info' => ['text' => 'cyan'], + 'debug' => ['text' => 'yellow'], + 'success' => ['text' => 'green'], + 'comment' => ['text' => 'blue'], + 'question' => ['text' => 'magenta'], + 'notice' => ['text' => 'cyan'] + ]; + + /** + * Construct the output object. + * + * Checks for a pretty console environment. Ansicon and ConEmu allows + * pretty consoles on windows, and is supported. + * + * @param string $stream The identifier of the stream to write output to. + */ + public function __construct($stream = 'php://stdout') + { + $this->_output = fopen($stream, 'wb'); + + if ((DIRECTORY_SEPARATOR === '\\' && !(bool)env('ANSICON') && env('ConEmuANSI') !== 'ON') || + (function_exists('posix_isatty') && !posix_isatty($this->_output)) + ) { + $this->_outputAs = self::PLAIN; + } + } + + /** + * Outputs a single or multiple messages to stdout or stderr. If no parameters + * are passed, outputs just a newline. + * + * @param string|array $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return int|bool The number of bytes returned from writing to output. + */ + public function write($message, $newlines = 1) + { + if (is_array($message)) { + $message = implode(static::LF, $message); + } + + return $this->_write($this->styleText($message . str_repeat(static::LF, $newlines))); + } + + /** + * Apply styling to text. + * + * @param string $text Text with styling tags. + * @return string String with color codes added. + */ + public function styleText($text) + { + if ($this->_outputAs == static::RAW) { + return $text; + } + if ($this->_outputAs == static::PLAIN) { + $tags = implode('|', array_keys(static::$_styles)); + + return preg_replace('##', '', $text); + } + + return preg_replace_callback( + '/<(?P[a-z0-9-_]+)>(?P.*?)<\/(\1)>/ims', + [$this, '_replaceTags'], + $text + ); + } + + /** + * Replace tags with color codes. + * + * @param array $matches An array of matches to replace. + * @return string + */ + protected function _replaceTags($matches) + { + $style = $this->styles($matches['tag']); + if (empty($style)) { + return '<' . $matches['tag'] . '>' . $matches['text'] . ''; + } + + $styleInfo = []; + if (!empty($style['text']) && isset(static::$_foregroundColors[$style['text']])) { + $styleInfo[] = static::$_foregroundColors[$style['text']]; + } + if (!empty($style['background']) && isset(static::$_backgroundColors[$style['background']])) { + $styleInfo[] = static::$_backgroundColors[$style['background']]; + } + unset($style['text'], $style['background']); + foreach ($style as $option => $value) { + if ($value) { + $styleInfo[] = static::$_options[$option]; + } + } + + return "\033[" . implode($styleInfo, ';') . 'm' . $matches['text'] . "\033[0m"; + } + + /** + * Writes a message to the output stream. + * + * @param string $message Message to write. + * @return int|bool The number of bytes returned from writing to output. + */ + protected function _write($message) + { + return fwrite($this->_output, $message); + } + + /** + * Get the current styles offered, or append new ones in. + * + * ### Get a style definition + * + * ``` + * $output->styles('error'); + * ``` + * + * ### Get all the style definitions + * + * ``` + * $output->styles(); + * ``` + * + * ### Create or modify an existing style + * + * ``` + * $output->styles('annoy', ['text' => 'purple', 'background' => 'yellow', 'blink' => true]); + * ``` + * + * ### Remove a style + * + * ``` + * $this->output->styles('annoy', false); + * ``` + * + * @param string|null $style The style to get or create. + * @param array|bool|null $definition The array definition of the style to change or create a style + * or false to remove a style. + * @return mixed If you are getting styles, the style or null will be returned. If you are creating/modifying + * styles true will be returned. + */ + public function styles($style = null, $definition = null) + { + if ($style === null && $definition === null) { + return static::$_styles; + } + if (is_string($style) && $definition === null) { + return isset(static::$_styles[$style]) ? static::$_styles[$style] : null; + } + if ($definition === false) { + unset(static::$_styles[$style]); + + return true; + } + static::$_styles[$style] = $definition; + + return true; + } + + /** + * Get the output type on how formatting tags are treated. + * + * @return int + */ + public function getOutputAs() + { + return $this->_outputAs; + } + + /** + * Set the output type on how formatting tags are treated. + * + * @param int $type The output type to use. Should be one of the class constants. + * @return void + * @throws \InvalidArgumentException in case of a not supported output type. + */ + public function setOutputAs($type) + { + if (!in_array($type, [self::RAW, self::PLAIN, self::COLOR], true)) { + throw new InvalidArgumentException(sprintf('Invalid output type "%s".', $type)); + } + + $this->_outputAs = $type; + } + + /** + * Get/Set the output type to use. The output type how formatting tags are treated. + * + * @deprecated 3.5.0 Use getOutputAs()/setOutputAs() instead. + * @param int|null $type The output type to use. Should be one of the class constants. + * @return int|null Either null or the value if getting. + */ + public function outputAs($type = null) + { + deprecationWarning( + 'ConsoleOutput::outputAs() is deprecated. ' . + 'Use ConsoleOutput::setOutputAs()/getOutputAs() instead.' + ); + if ($type === null) { + return $this->_outputAs; + } + $this->_outputAs = $type; + } + + /** + * Clean up and close handles + */ + public function __destruct() + { + if (is_resource($this->_output)) { + fclose($this->_output); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/Exception/ConsoleException.php b/app/vendor/cakephp/cakephp/src/Console/Exception/ConsoleException.php new file mode 100644 index 000000000..445af8d90 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/Exception/ConsoleException.php @@ -0,0 +1,24 @@ +addPlugin(), you may need to update bin/cake.php to match https://github.com/cakephp/app/tree/master/bin/cake.php'; +} diff --git a/app/vendor/cakephp/cakephp/src/Console/Exception/MissingShellMethodException.php b/app/vendor/cakephp/cakephp/src/Console/Exception/MissingShellMethodException.php new file mode 100644 index 000000000..7c9251f3f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/Exception/MissingShellMethodException.php @@ -0,0 +1,24 @@ +help($command, 'xml'); is usually + * how you would access help. Or via the `--help=xml` option on the command line. + * + * Xml output is useful for integration with other tools like IDE's or other build tools. + */ +class HelpFormatter +{ + + /** + * The maximum number of arguments shown when generating usage. + * + * @var int + */ + protected $_maxArgs = 6; + + /** + * The maximum number of options shown when generating usage. + * + * @var int + */ + protected $_maxOptions = 6; + + /** + * Option parser. + * + * @var \Cake\Console\ConsoleOptionParser + */ + protected $_parser; + + /** + * Alias to display in the output. + * + * @var string + */ + protected $_alias = 'cake'; + + /** + * Build the help formatter for an OptionParser + * + * @param \Cake\Console\ConsoleOptionParser $parser The option parser help is being generated for. + */ + public function __construct(ConsoleOptionParser $parser) + { + $this->_parser = $parser; + } + + /** + * Set the alias + * + * @param string $alias The alias + * @return void + * @throws \Cake\Console\Exception\ConsoleException When alias is not a string. + */ + public function setAlias($alias) + { + if (is_string($alias)) { + $this->_alias = $alias; + } else { + throw new ConsoleException('Alias must be of type string.'); + } + } + + /** + * Get the help as formatted text suitable for output on the command line. + * + * @param int $width The width of the help output. + * @return string + */ + public function text($width = 72) + { + $parser = $this->_parser; + $out = []; + $description = $parser->getDescription(); + if (!empty($description)) { + $out[] = Text::wrap($description, $width); + $out[] = ''; + } + $out[] = 'Usage:'; + $out[] = $this->_generateUsage(); + $out[] = ''; + $subcommands = $parser->subcommands(); + if (!empty($subcommands)) { + $out[] = 'Subcommands:'; + $out[] = ''; + $max = $this->_getMaxLength($subcommands) + 2; + foreach ($subcommands as $command) { + $out[] = Text::wrapBlock($command->help($max), [ + 'width' => $width, + 'indent' => str_repeat(' ', $max), + 'indentAt' => 1 + ]); + } + $out[] = ''; + $out[] = sprintf('To see help on a subcommand use `' . $this->_alias . ' %s [subcommand] --help`', $parser->getCommand()); + $out[] = ''; + } + + $options = $parser->options(); + if (!empty($options)) { + $max = $this->_getMaxLength($options) + 8; + $out[] = 'Options:'; + $out[] = ''; + foreach ($options as $option) { + $out[] = Text::wrapBlock($option->help($max), [ + 'width' => $width, + 'indent' => str_repeat(' ', $max), + 'indentAt' => 1 + ]); + } + $out[] = ''; + } + + $arguments = $parser->arguments(); + if (!empty($arguments)) { + $max = $this->_getMaxLength($arguments) + 2; + $out[] = 'Arguments:'; + $out[] = ''; + foreach ($arguments as $argument) { + $out[] = Text::wrapBlock($argument->help($max), [ + 'width' => $width, + 'indent' => str_repeat(' ', $max), + 'indentAt' => 1 + ]); + } + $out[] = ''; + } + $epilog = $parser->getEpilog(); + if (!empty($epilog)) { + $out[] = Text::wrap($epilog, $width); + $out[] = ''; + } + + return implode("\n", $out); + } + + /** + * Generate the usage for a shell based on its arguments and options. + * Usage strings favor short options over the long ones. and optional args will + * be indicated with [] + * + * @return string + */ + protected function _generateUsage() + { + $usage = [$this->_alias . ' ' . $this->_parser->getCommand()]; + $subcommands = $this->_parser->subcommands(); + if (!empty($subcommands)) { + $usage[] = '[subcommand]'; + } + $options = []; + foreach ($this->_parser->options() as $option) { + $options[] = $option->usage(); + } + if (count($options) > $this->_maxOptions) { + $options = ['[options]']; + } + $usage = array_merge($usage, $options); + $args = []; + foreach ($this->_parser->arguments() as $argument) { + $args[] = $argument->usage(); + } + if (count($args) > $this->_maxArgs) { + $args = ['[arguments]']; + } + $usage = array_merge($usage, $args); + + return implode(' ', $usage); + } + + /** + * Iterate over a collection and find the longest named thing. + * + * @param array $collection The collection to find a max length of. + * @return int + */ + protected function _getMaxLength($collection) + { + $max = 0; + foreach ($collection as $item) { + $max = (strlen($item->name()) > $max) ? strlen($item->name()) : $max; + } + + return $max; + } + + /** + * Get the help as an xml string. + * + * @param bool $string Return the SimpleXml object or a string. Defaults to true. + * @return string|\SimpleXMLElement See $string + */ + public function xml($string = true) + { + $parser = $this->_parser; + $xml = new SimpleXMLElement(''); + $xml->addChild('command', $parser->getCommand()); + $xml->addChild('description', $parser->getDescription()); + + $subcommands = $xml->addChild('subcommands'); + foreach ($parser->subcommands() as $command) { + $command->xml($subcommands); + } + $options = $xml->addChild('options'); + foreach ($parser->options() as $option) { + $option->xml($options); + } + $arguments = $xml->addChild('arguments'); + foreach ($parser->arguments() as $argument) { + $argument->xml($arguments); + } + $xml->addChild('epilog', $parser->getEpilog()); + + return $string ? $xml->asXML() : $xml; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/Helper.php b/app/vendor/cakephp/cakephp/src/Console/Helper.php new file mode 100644 index 000000000..e498f3adb --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/Helper.php @@ -0,0 +1,63 @@ +_io = $io; + $this->setConfig($config); + } + + /** + * This method should output content using `$this->_io`. + * + * @param array $args The arguments for the helper. + * @return void + */ + abstract public function output($args); +} diff --git a/app/vendor/cakephp/cakephp/src/Console/HelperRegistry.php b/app/vendor/cakephp/cakephp/src/Console/HelperRegistry.php new file mode 100644 index 000000000..c832d8ab7 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/HelperRegistry.php @@ -0,0 +1,100 @@ +_io = $io; + } + + /** + * Resolve a helper classname. + * + * Will prefer helpers defined in Command\Helper over those + * defined in Shell\Helper. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * + * @param string $class Partial classname to resolve. + * @return string|false Either the correct classname or false. + */ + protected function _resolveClassName($class) + { + $name = App::className($class, 'Command/Helper', 'Helper'); + if ($name) { + return $name; + } + + return App::className($class, 'Shell/Helper', 'Helper'); + } + + /** + * Throws an exception when a helper is missing. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * and Cake\Core\ObjectRegistry::unload() + * + * @param string $class The classname that is missing. + * @param string $plugin The plugin the helper is missing in. + * @return void + * @throws \Cake\Console\Exception\MissingHelperException + */ + protected function _throwMissingClassError($class, $plugin) + { + throw new MissingHelperException([ + 'class' => $class, + 'plugin' => $plugin + ]); + } + + /** + * Create the helper instance. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * + * @param string $class The classname to create. + * @param string $alias The alias of the helper. + * @param array $settings An array of settings to use for the helper. + * @return \Cake\Console\Helper The constructed helper class. + */ + protected function _create($class, $alias, $settings) + { + return new $class($this->_io, $settings); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/Shell.php b/app/vendor/cakephp/cakephp/src/Console/Shell.php new file mode 100644 index 000000000..b07b9ca43 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/Shell.php @@ -0,0 +1,1011 @@ +name) { + list(, $class) = namespaceSplit(get_class($this)); + $this->name = str_replace(['Shell', 'Task'], '', $class); + } + $this->_io = $io ?: new ConsoleIo(); + $this->_tableLocator = $locator; + + $this->modelFactory('Table', [$this->getTableLocator(), 'get']); + $this->Tasks = new TaskRegistry($this); + + $this->_mergeVars( + ['tasks'], + ['associative' => ['tasks']] + ); + + if (isset($this->modelClass)) { + $this->loadModel(); + } + } + + /** + * Set the root command name for help output. + * + * @param string $name The name of the root command. + * @return $this + */ + public function setRootName($name) + { + $this->rootName = (string)$name; + + return $this; + } + + /** + * Get the io object for this shell. + * + * @return \Cake\Console\ConsoleIo The current ConsoleIo object. + */ + public function getIo() + { + return $this->_io; + } + + /** + * Set the io object for this shell. + * + * @param \Cake\Console\ConsoleIo $io The ConsoleIo object to use. + * @return void + */ + public function setIo(ConsoleIo $io) + { + $this->_io = $io; + } + + /** + * Get/Set the io object for this shell. + * + * @deprecated 3.5.0 Use getIo()/setIo() instead. + * @param \Cake\Console\ConsoleIo|null $io The ConsoleIo object to use. + * @return \Cake\Console\ConsoleIo The current ConsoleIo object. + */ + public function io(ConsoleIo $io = null) + { + deprecationWarning( + 'Shell::io() is deprecated. ' . + 'Use Shell::setIo()/getIo() instead.' + ); + if ($io !== null) { + $this->_io = $io; + } + + return $this->_io; + } + + /** + * Initializes the Shell + * acts as constructor for subclasses + * allows configuration of tasks prior to shell execution + * + * @return void + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#Cake\Console\ConsoleOptionParser::initialize + */ + public function initialize() + { + $this->loadTasks(); + } + + /** + * Starts up the Shell and displays the welcome message. + * Allows for checking and configuring prior to command or main execution + * + * Override this method if you want to remove the welcome information, + * or otherwise modify the pre-command flow. + * + * @return void + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#Cake\Console\ConsoleOptionParser::startup + */ + public function startup() + { + if (!$this->param('requested')) { + $this->_welcome(); + } + } + + /** + * Displays a header for the shell + * + * @return void + */ + protected function _welcome() + { + } + + /** + * Loads tasks defined in public $tasks + * + * @return bool + */ + public function loadTasks() + { + if ($this->tasks === true || empty($this->tasks) || empty($this->Tasks)) { + return true; + } + $this->_taskMap = $this->Tasks->normalizeArray((array)$this->tasks); + $this->taskNames = array_merge($this->taskNames, array_keys($this->_taskMap)); + + $this->_validateTasks(); + + return true; + } + + /** + * Checks that the tasks in the task map are actually available + * + * @throws \RuntimeException + * @return void + */ + protected function _validateTasks() + { + foreach ($this->_taskMap as $taskName => $task) { + $class = App::className($task['class'], 'Shell/Task', 'Task'); + if (!class_exists($class)) { + throw new RuntimeException(sprintf( + 'Task `%s` not found. Maybe you made a typo or a plugin is missing or not loaded?', + $taskName + )); + } + } + } + + /** + * Check to see if this shell has a task with the provided name. + * + * @param string $task The task name to check. + * @return bool Success + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#shell-tasks + */ + public function hasTask($task) + { + return isset($this->_taskMap[Inflector::camelize($task)]); + } + + /** + * Check to see if this shell has a callable method by the given name. + * + * @param string $name The method name to check. + * @return bool + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#shell-tasks + */ + public function hasMethod($name) + { + try { + $method = new ReflectionMethod($this, $name); + if (!$method->isPublic()) { + return false; + } + + return $method->getDeclaringClass()->name !== 'Cake\Console\Shell'; + } catch (ReflectionException $e) { + return false; + } + } + + /** + * Dispatch a command to another Shell. Similar to Object::requestAction() + * but intended for running shells from other shells. + * + * ### Usage: + * + * With a string command: + * + * ``` + * return $this->dispatchShell('schema create DbAcl'); + * ``` + * + * Avoid using this form if you have string arguments, with spaces in them. + * The dispatched will be invoked incorrectly. Only use this form for simple + * command dispatching. + * + * With an array command: + * + * ``` + * return $this->dispatchShell('schema', 'create', 'i18n', '--dry'); + * ``` + * + * With an array having two key / value pairs: + * - `command` can accept either a string or an array. Represents the command to dispatch + * - `extra` can accept an array of extra parameters to pass on to the dispatcher. This + * parameters will be available in the `param` property of the called `Shell` + * + * `return $this->dispatchShell([ + * 'command' => 'schema create DbAcl', + * 'extra' => ['param' => 'value'] + * ]);` + * + * or + * + * `return $this->dispatchShell([ + * 'command' => ['schema', 'create', 'DbAcl'], + * 'extra' => ['param' => 'value'] + * ]);` + * + * @return int The cli command exit code. 0 is success. + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#invoking-other-shells-from-your-shell + */ + public function dispatchShell() + { + list($args, $extra) = $this->parseDispatchArguments(func_get_args()); + + if (!isset($extra['requested'])) { + $extra['requested'] = true; + } + + $dispatcher = new ShellDispatcher($args, false); + + return $dispatcher->dispatch($extra); + } + + /** + * Parses the arguments for the dispatchShell() method. + * + * @param array $args Arguments fetch from the dispatchShell() method with + * func_get_args() + * @return array First value has to be an array of the command arguments. + * Second value has to be an array of extra parameter to pass on to the dispatcher + */ + public function parseDispatchArguments($args) + { + $extra = []; + + if (is_string($args[0]) && count($args) === 1) { + $args = explode(' ', $args[0]); + + return [$args, $extra]; + } + + if (is_array($args[0]) && !empty($args[0]['command'])) { + $command = $args[0]['command']; + if (is_string($command)) { + $command = explode(' ', $command); + } + + if (!empty($args[0]['extra'])) { + $extra = $args[0]['extra']; + } + + return [$command, $extra]; + } + + return [$args, $extra]; + } + + /** + * Runs the Shell with the provided argv. + * + * Delegates calls to Tasks and resolves methods inside the class. Commands are looked + * up with the following order: + * + * - Method on the shell. + * - Matching task name. + * - `main()` method. + * + * If a shell implements a `main()` method, all missing method calls will be sent to + * `main()` with the original method name in the argv. + * + * For tasks to be invoked they *must* be exposed as subcommands. If you define any subcommands, + * you must define all the subcommands your shell needs, whether they be methods on this class + * or methods on tasks. + * + * @param array $argv Array of arguments to run the shell with. This array should be missing the shell name. + * @param bool $autoMethod Set to true to allow any public method to be called even if it + * was not defined as a subcommand. This is used by ShellDispatcher to make building simple shells easy. + * @param array $extra Extra parameters that you can manually pass to the Shell + * to be dispatched. + * Built-in extra parameter is : + * - `requested` : if used, will prevent the Shell welcome message to be displayed + * @return int|bool|null + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#the-cakephp-console + */ + public function runCommand($argv, $autoMethod = false, $extra = []) + { + $command = isset($argv[0]) ? Inflector::underscore($argv[0]) : null; + $this->OptionParser = $this->getOptionParser(); + try { + list($this->params, $this->args) = $this->OptionParser->parse($argv); + } catch (ConsoleException $e) { + $this->err('Error: ' . $e->getMessage()); + + return false; + } + + if (!empty($extra) && is_array($extra)) { + $this->params = array_merge($this->params, $extra); + } + $this->_setOutputLevel(); + $this->command = $command; + if (!empty($this->params['help'])) { + return $this->_displayHelp($command); + } + + $subcommands = $this->OptionParser->subcommands(); + $method = Inflector::camelize($command); + $isMethod = $this->hasMethod($method); + + if ($isMethod && $autoMethod && count($subcommands) === 0) { + array_shift($this->args); + $this->startup(); + + return $this->$method(...$this->args); + } + + if ($isMethod && isset($subcommands[$command])) { + $this->startup(); + + return $this->$method(...$this->args); + } + + if ($this->hasTask($command) && isset($subcommands[$command])) { + $this->startup(); + array_shift($argv); + + return $this->{$method}->runCommand($argv, false, ['requested' => true]); + } + + if ($this->hasMethod('main')) { + $this->command = 'main'; + $this->startup(); + + return $this->main(...$this->args); + } + + $this->err('No subcommand provided. Choose one of the available subcommands.', 2); + $this->_io->err($this->OptionParser->help($command)); + + return false; + } + + /** + * Set the output level based on the parameters. + * + * This reconfigures both the output level for out() + * and the configured stdout/stderr logging + * + * @return void + */ + protected function _setOutputLevel() + { + $this->_io->setLoggers(ConsoleIo::NORMAL); + if (!empty($this->params['quiet'])) { + $this->_io->level(ConsoleIo::QUIET); + $this->_io->setLoggers(ConsoleIo::QUIET); + } + if (!empty($this->params['verbose'])) { + $this->_io->level(ConsoleIo::VERBOSE); + $this->_io->setLoggers(ConsoleIo::VERBOSE); + } + } + + /** + * Display the help in the correct format + * + * @param string $command The command to get help for. + * @return int|bool The number of bytes returned from writing to stdout. + */ + protected function _displayHelp($command) + { + $format = 'text'; + if (!empty($this->args[0]) && $this->args[0] === 'xml') { + $format = 'xml'; + $this->_io->setOutputAs(ConsoleOutput::RAW); + } else { + $this->_welcome(); + } + + $subcommands = $this->OptionParser->subcommands(); + $command = isset($subcommands[$command]) ? $command : null; + + return $this->out($this->OptionParser->help($command, $format)); + } + + /** + * Gets the option parser instance and configures it. + * + * By overriding this method you can configure the ConsoleOptionParser before returning it. + * + * @return \Cake\Console\ConsoleOptionParser + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#configuring-options-and-generating-help + */ + public function getOptionParser() + { + $name = ($this->plugin ? $this->plugin . '.' : '') . $this->name; + $parser = new ConsoleOptionParser($name); + $parser->setRootName($this->rootName); + + return $parser; + } + + /** + * Overload get for lazy building of tasks + * + * @param string $name The task to get. + * @return \Cake\Console\Shell Object of Task + */ + public function __get($name) + { + if (empty($this->{$name}) && in_array($name, $this->taskNames)) { + $properties = $this->_taskMap[$name]; + $this->{$name} = $this->Tasks->load($properties['class'], $properties['config']); + $this->{$name}->args =& $this->args; + $this->{$name}->params =& $this->params; + $this->{$name}->initialize(); + $this->{$name}->loadTasks(); + } + + return $this->{$name}; + } + + /** + * Safely access the values in $this->params. + * + * @param string $name The name of the parameter to get. + * @return string|bool|null Value. Will return null if it doesn't exist. + */ + public function param($name) + { + if (!isset($this->params[$name])) { + return null; + } + + return $this->params[$name]; + } + + /** + * Prompts the user for input, and returns it. + * + * @param string $prompt Prompt text. + * @param string|array|null $options Array or string of options. + * @param string|null $default Default input value. + * @return mixed Either the default value, or the user-provided input. + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::in + */ + public function in($prompt, $options = null, $default = null) + { + if (!$this->interactive) { + return $default; + } + if ($options) { + return $this->_io->askChoice($prompt, $options, $default); + } + + return $this->_io->ask($prompt, $default); + } + + /** + * Wrap a block of text. + * Allows you to set the width, and indenting on a block of text. + * + * ### Options + * + * - `width` The width to wrap to. Defaults to 72 + * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true. + * - `indent` Indent the text with the string provided. Defaults to null. + * + * @param string $text Text the text to format. + * @param int|array $options Array of options to use, or an integer to wrap the text to. + * @return string Wrapped / indented text + * @see \Cake\Utility\Text::wrap() + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::wrapText + */ + public function wrapText($text, $options = []) + { + return Text::wrap($text, $options); + } + + /** + * Output at the verbose level. + * + * @param string|array $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return int|bool The number of bytes returned from writing to stdout. + */ + public function verbose($message, $newlines = 1) + { + return $this->_io->verbose($message, $newlines); + } + + /** + * Output at all levels. + * + * @param string|array $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return int|bool The number of bytes returned from writing to stdout. + */ + public function quiet($message, $newlines = 1) + { + return $this->_io->quiet($message, $newlines); + } + + /** + * Outputs a single or multiple messages to stdout. If no parameters + * are passed outputs just a newline. + * + * ### Output levels + * + * There are 3 built-in output level. Shell::QUIET, Shell::NORMAL, Shell::VERBOSE. + * The verbose and quiet output levels, map to the `verbose` and `quiet` output switches + * present in most shells. Using Shell::QUIET for a message means it will always display. + * While using Shell::VERBOSE means it will only display when verbose output is toggled. + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @param int $level The message's output level, see above. + * @return int|bool The number of bytes returned from writing to stdout. + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::out + */ + public function out($message = null, $newlines = 1, $level = Shell::NORMAL) + { + return $this->_io->out($message, $newlines, $level); + } + + /** + * Outputs a single or multiple error messages to stderr. If no parameters + * are passed outputs just a newline. + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return int|bool The number of bytes returned from writing to stderr. + */ + public function err($message = null, $newlines = 1) + { + return $this->_io->error($message, $newlines); + } + + /** + * Convenience method for out() that wraps message between tag + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @param int $level The message's output level, see above. + * @return int|bool The number of bytes returned from writing to stdout. + * @see https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::out + */ + public function info($message = null, $newlines = 1, $level = Shell::NORMAL) + { + return $this->_io->info($message, $newlines, $level); + } + + /** + * Convenience method for err() that wraps message between tag + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return int|bool The number of bytes returned from writing to stderr. + * @see https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::err + */ + public function warn($message = null, $newlines = 1) + { + return $this->_io->warning($message, $newlines); + } + + /** + * Convenience method for out() that wraps message between tag + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @param int $level The message's output level, see above. + * @return int|bool The number of bytes returned from writing to stdout. + * @see https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::out + */ + public function success($message = null, $newlines = 1, $level = Shell::NORMAL) + { + return $this->_io->success($message, $newlines, $level); + } + + /** + * Wraps a message with a given message type, e.g. + * + * @param string $messageType The message type, e.g. "warning". + * @param string|array $message The message to wrap. + * @return array|string The message wrapped with the given message type. + * @deprecated 3.6.0 Will be removed in 4.0.0 as it is no longer in use. + */ + protected function wrapMessageWithType($messageType, $message) + { + deprecationWarning( + 'Shell::wrapMessageWithType() is deprecated. ' . + 'Use output methods on ConsoleIo instead.' + ); + if (is_array($message)) { + foreach ($message as $k => $v) { + $message[$k] = "<$messageType>" . $v . ""; + } + } else { + $message = "<$messageType>" . $message . ""; + } + + return $message; + } + + /** + * Returns a single or multiple linefeeds sequences. + * + * @param int $multiplier Number of times the linefeed sequence should be repeated + * @return string + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::nl + */ + public function nl($multiplier = 1) + { + return $this->_io->nl($multiplier); + } + + /** + * Outputs a series of minus characters to the standard output, acts as a visual separator. + * + * @param int $newlines Number of newlines to pre- and append + * @param int $width Width of the line, defaults to 63 + * @return void + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::hr + */ + public function hr($newlines = 0, $width = 63) + { + $this->_io->hr($newlines, $width); + } + + /** + * Displays a formatted error message + * and exits the application with status code 1 + * + * @param string $message The error message + * @param int $exitCode The exit code for the shell task. + * @throws \Cake\Console\Exception\StopException + * @return void + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#styling-output + */ + public function abort($message, $exitCode = self::CODE_ERROR) + { + $this->_io->err('' . $message . ''); + throw new StopException($message, $exitCode); + } + + /** + * Displays a formatted error message + * and exits the application with status code 1 + * + * @param string $title Title of the error + * @param string|null $message An optional error message + * @param int $exitCode The exit code for the shell task. + * @throws \Cake\Console\Exception\StopException + * @return int Error code + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#styling-output + * @deprecated 3.2.0 Use Shell::abort() instead. + */ + public function error($title, $message = null, $exitCode = self::CODE_ERROR) + { + deprecationWarning('Shell::error() is deprecated. `Use Shell::abort() instead.'); + $this->_io->err(sprintf('Error: %s', $title)); + + if (!empty($message)) { + $this->_io->err($message); + } + + $this->_stop($exitCode); + + return $exitCode; + } + + /** + * Clear the console + * + * @return void + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#console-output + */ + public function clear() + { + if (empty($this->params['noclear'])) { + if (DIRECTORY_SEPARATOR === '/') { + passthru('clear'); + } else { + passthru('cls'); + } + } + } + + /** + * Creates a file at given path + * + * @param string $path Where to put the file. + * @param string $contents Content to put in the file. + * @return bool Success + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#creating-files + */ + public function createFile($path, $contents) + { + $path = str_replace(DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, $path); + + $this->_io->out(); + + $fileExists = is_file($path); + if ($fileExists && empty($this->params['force']) && !$this->interactive) { + $this->_io->out('File exists, skipping.'); + + return false; + } + + if ($fileExists && $this->interactive && empty($this->params['force'])) { + $this->_io->out(sprintf('File `%s` exists', $path)); + $key = $this->_io->askChoice('Do you want to overwrite?', ['y', 'n', 'a', 'q'], 'n'); + + if (strtolower($key) === 'q') { + $this->_io->out('Quitting.', 2); + $this->_stop(); + + return false; + } + if (strtolower($key) === 'a') { + $this->params['force'] = true; + $key = 'y'; + } + if (strtolower($key) !== 'y') { + $this->_io->out(sprintf('Skip `%s`', $path), 2); + + return false; + } + } else { + $this->out(sprintf('Creating file %s', $path)); + } + + $File = new File($path, true); + + try { + if ($File->exists() && $File->writable()) { + $File->write($contents); + $this->_io->out(sprintf('Wrote `%s`', $path)); + + return true; + } + + $this->_io->err(sprintf('Could not write to `%s`.', $path), 2); + + return false; + } finally { + $File->close(); + } + } + + /** + * Makes absolute file path easier to read + * + * @param string $file Absolute file path + * @return string short path + * @link https://book.cakephp.org/3.0/en/console-and-shells.html#Shell::shortPath + */ + public function shortPath($file) + { + $shortPath = str_replace(ROOT, null, $file); + $shortPath = str_replace('..' . DIRECTORY_SEPARATOR, '', $shortPath); + $shortPath = str_replace(DIRECTORY_SEPARATOR, '/', $shortPath); + + return str_replace('//', DIRECTORY_SEPARATOR, $shortPath); + } + + /** + * Render a Console Helper + * + * Create and render the output for a helper object. If the helper + * object has not already been loaded, it will be loaded and constructed. + * + * @param string $name The name of the helper to render + * @param array $settings Configuration data for the helper. + * @return \Cake\Console\Helper The created helper instance. + */ + public function helper($name, array $settings = []) + { + return $this->_io->helper($name, $settings); + } + + /** + * Stop execution of the current script. + * Raises a StopException to try and halt the execution. + * + * @param int|string $status see https://secure.php.net/exit for values + * @throws \Cake\Console\Exception\StopException + * @return void + */ + protected function _stop($status = self::CODE_SUCCESS) + { + throw new StopException('Halting error reached', $status); + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + return [ + 'name' => $this->name, + 'plugin' => $this->plugin, + 'command' => $this->command, + 'tasks' => $this->tasks, + 'params' => $this->params, + 'args' => $this->args, + 'interactive' => $this->interactive, + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/ShellDispatcher.php b/app/vendor/cakephp/cakephp/src/Console/ShellDispatcher.php new file mode 100644 index 000000000..03399ca56 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/ShellDispatcher.php @@ -0,0 +1,416 @@ +args = (array)$args; + + $this->addShortPluginAliases(); + + if ($bootstrap) { + $this->_initEnvironment(); + } + } + + /** + * Add an alias for a shell command. + * + * Aliases allow you to call shells by alternate names. This is most + * useful when dealing with plugin shells that you want to have shorter + * names for. + * + * If you re-use an alias the last alias set will be the one available. + * + * ### Usage + * + * Aliasing a shell named ClassName: + * + * ``` + * $this->alias('alias', 'ClassName'); + * ``` + * + * Getting the original name for a given alias: + * + * ``` + * $this->alias('alias'); + * ``` + * + * @param string $short The new short name for the shell. + * @param string|null $original The original full name for the shell. + * @return string|false The aliased class name, or false if the alias does not exist + */ + public static function alias($short, $original = null) + { + $short = Inflector::camelize($short); + if ($original) { + static::$_aliases[$short] = $original; + } + + return isset(static::$_aliases[$short]) ? static::$_aliases[$short] : false; + } + + /** + * Clear any aliases that have been set. + * + * @return void + */ + public static function resetAliases() + { + static::$_aliases = []; + } + + /** + * Run the dispatcher + * + * @param array $argv The argv from PHP + * @param array $extra Extra parameters + * @return int The exit code of the shell process. + */ + public static function run($argv, $extra = []) + { + $dispatcher = new ShellDispatcher($argv); + + return $dispatcher->dispatch($extra); + } + + /** + * Defines current working environment. + * + * @return void + * @throws \Cake\Core\Exception\Exception + */ + protected function _initEnvironment() + { + if (!$this->_bootstrap()) { + $message = "Unable to load CakePHP core.\nMake sure Cake exists in " . CAKE_CORE_INCLUDE_PATH; + throw new Exception($message); + } + + if (function_exists('ini_set')) { + ini_set('html_errors', '0'); + ini_set('implicit_flush', '1'); + ini_set('max_execution_time', '0'); + } + + $this->shiftArgs(); + } + + /** + * Initializes the environment and loads the CakePHP core. + * + * @return bool Success. + */ + protected function _bootstrap() + { + if (!Configure::read('App.fullBaseUrl')) { + Configure::write('App.fullBaseUrl', 'http://localhost'); + } + + return true; + } + + /** + * Dispatches a CLI request + * + * Converts a shell command result into an exit code. Null/True + * are treated as success. All other return values are an error. + * + * @param array $extra Extra parameters that you can manually pass to the Shell + * to be dispatched. + * Built-in extra parameter is : + * - `requested` : if used, will prevent the Shell welcome message to be displayed + * @return int The cli command exit code. 0 is success. + */ + public function dispatch($extra = []) + { + try { + $result = $this->_dispatch($extra); + } catch (StopException $e) { + return $e->getCode(); + } + if ($result === null || $result === true) { + return Shell::CODE_SUCCESS; + } + if (is_int($result)) { + return $result; + } + + return Shell::CODE_ERROR; + } + + /** + * Dispatch a request. + * + * @param array $extra Extra parameters that you can manually pass to the Shell + * to be dispatched. + * Built-in extra parameter is : + * - `requested` : if used, will prevent the Shell welcome message to be displayed + * @return bool|int|null + * @throws \Cake\Console\Exception\MissingShellMethodException + */ + protected function _dispatch($extra = []) + { + $shell = $this->shiftArgs(); + + if (!$shell) { + $this->help(); + + return false; + } + if (in_array($shell, ['help', '--help', '-h'])) { + $this->help(); + + return true; + } + if (in_array($shell, ['version', '--version'])) { + $this->version(); + + return true; + } + + $Shell = $this->findShell($shell); + + $Shell->initialize(); + + return $Shell->runCommand($this->args, true, $extra); + } + + /** + * For all loaded plugins, add a short alias + * + * This permits a plugin which implements a shell of the same name to be accessed + * Using the shell name alone + * + * @return array the resultant list of aliases + */ + public function addShortPluginAliases() + { + $plugins = Plugin::loaded(); + + $io = new ConsoleIo(); + $task = new CommandTask($io); + $io->setLoggers(false); + $list = $task->getShellList() + ['app' => []]; + $fixed = array_flip($list['app']) + array_flip($list['CORE']); + $aliases = $others = []; + + foreach ($plugins as $plugin) { + if (!isset($list[$plugin])) { + continue; + } + + foreach ($list[$plugin] as $shell) { + $aliases += [$shell => $plugin]; + if (!isset($others[$shell])) { + $others[$shell] = [$plugin]; + } else { + $others[$shell] = array_merge($others[$shell], [$plugin]); + } + } + } + + foreach ($aliases as $shell => $plugin) { + if (isset($fixed[$shell])) { + Log::write( + 'debug', + "command '$shell' in plugin '$plugin' was not aliased, conflicts with another shell", + ['shell-dispatcher'] + ); + continue; + } + + $other = static::alias($shell); + if ($other) { + $other = $aliases[$shell]; + if ($other !== $plugin) { + Log::write( + 'debug', + "command '$shell' in plugin '$plugin' was not aliased, conflicts with '$other'", + ['shell-dispatcher'] + ); + } + continue; + } + + if (isset($others[$shell])) { + $conflicts = array_diff($others[$shell], [$plugin]); + if (count($conflicts) > 0) { + $conflictList = implode("', '", $conflicts); + Log::write( + 'debug', + "command '$shell' in plugin '$plugin' was not aliased, conflicts with '$conflictList'", + ['shell-dispatcher'] + ); + } + } + + static::alias($shell, "$plugin.$shell"); + } + + return static::$_aliases; + } + + /** + * Get shell to use, either plugin shell or application shell + * + * All paths in the loaded shell paths are searched, handles alias + * dereferencing + * + * @param string $shell Optionally the name of a plugin + * @return \Cake\Console\Shell A shell instance. + * @throws \Cake\Console\Exception\MissingShellException when errors are encountered. + */ + public function findShell($shell) + { + $className = $this->_shellExists($shell); + if (!$className) { + $shell = $this->_handleAlias($shell); + $className = $this->_shellExists($shell); + } + + if (!$className) { + throw new MissingShellException([ + 'class' => $shell, + ]); + } + + return $this->_createShell($className, $shell); + } + + /** + * If the input matches an alias, return the aliased shell name + * + * @param string $shell Optionally the name of a plugin or alias + * @return string Shell name with plugin prefix + */ + protected function _handleAlias($shell) + { + $aliased = static::alias($shell); + if ($aliased) { + $shell = $aliased; + } + + $class = array_map('Cake\Utility\Inflector::camelize', explode('.', $shell)); + + return implode('.', $class); + } + + /** + * Check if a shell class exists for the given name. + * + * @param string $shell The shell name to look for. + * @return string|bool Either the classname or false. + */ + protected function _shellExists($shell) + { + $class = App::className($shell, 'Shell', 'Shell'); + if (class_exists($class)) { + return $class; + } + + return false; + } + + /** + * Create the given shell name, and set the plugin property + * + * @param string $className The class name to instantiate + * @param string $shortName The plugin-prefixed shell name + * @return \Cake\Console\Shell A shell instance. + */ + protected function _createShell($className, $shortName) + { + list($plugin) = pluginSplit($shortName); + $instance = new $className(); + $instance->plugin = trim($plugin, '.'); + + return $instance; + } + + /** + * Removes first argument and shifts other arguments up + * + * @return mixed Null if there are no arguments otherwise the shifted argument + */ + public function shiftArgs() + { + return array_shift($this->args); + } + + /** + * Shows console help. Performs an internal dispatch to the CommandList Shell + * + * @return void + */ + public function help() + { + $this->args = array_merge(['command_list'], $this->args); + $this->dispatch(); + } + + /** + * Prints the currently installed version of CakePHP. Performs an internal dispatch to the CommandList Shell + * + * @return void + */ + public function version() + { + $this->args = array_merge(['command_list', '--version'], $this->args); + $this->dispatch(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Console/TaskRegistry.php b/app/vendor/cakephp/cakephp/src/Console/TaskRegistry.php new file mode 100644 index 000000000..1655d989a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Console/TaskRegistry.php @@ -0,0 +1,91 @@ +_Shell = $Shell; + } + + /** + * Resolve a task classname. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * + * @param string $class Partial classname to resolve. + * @return string|false Either the correct classname or false. + */ + protected function _resolveClassName($class) + { + return App::className($class, 'Shell/Task', 'Task'); + } + + /** + * Throws an exception when a task is missing. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * and Cake\Core\ObjectRegistry::unload() + * + * @param string $class The classname that is missing. + * @param string $plugin The plugin the task is missing in. + * @return void + * @throws \Cake\Console\Exception\MissingTaskException + */ + protected function _throwMissingClassError($class, $plugin) + { + throw new MissingTaskException([ + 'class' => $class, + 'plugin' => $plugin + ]); + } + + /** + * Create the task instance. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * + * @param string $class The classname to create. + * @param string $alias The alias of the task. + * @param array $settings An array of settings to use for the task. + * @return \Cake\Console\Shell The constructed task class. + */ + protected function _create($class, $alias, $settings) + { + return new $class($this->_Shell->getIo()); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/Component.php b/app/vendor/cakephp/cakephp/src/Controller/Component.php new file mode 100644 index 000000000..408d18c2e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/Component.php @@ -0,0 +1,224 @@ +getSubject() to access the controller & request instead. + */ + public $request; + + /** + * Response object + * + * @var \Cake\Http\Response + * @deprecated 3.4.0 Storing references to the response is deprecated. Use Component::getController() + * or callback $event->getSubject() to access the controller & response instead. + */ + public $response; + + /** + * Component registry class used to lazy load components. + * + * @var \Cake\Controller\ComponentRegistry + */ + protected $_registry; + + /** + * Other Components this component uses. + * + * @var array + */ + public $components = []; + + /** + * Default config + * + * These are merged with user-provided config when the component is used. + * + * @var array + */ + protected $_defaultConfig = []; + + /** + * A component lookup table used to lazy load component objects. + * + * @var array + */ + protected $_componentMap = []; + + /** + * Constructor + * + * @param \Cake\Controller\ComponentRegistry $registry A ComponentRegistry this component can use to lazy load its components + * @param array $config Array of configuration settings. + */ + public function __construct(ComponentRegistry $registry, array $config = []) + { + $this->_registry = $registry; + $controller = $registry->getController(); + if ($controller) { + $this->request =& $controller->request; + $this->response =& $controller->response; + } + + $this->setConfig($config); + + if ($this->components) { + $this->_componentMap = $registry->normalizeArray($this->components); + } + $this->initialize($config); + } + + /** + * Get the controller this component is bound to. + * + * @return \Cake\Controller\Controller The bound controller. + */ + public function getController() + { + return $this->_registry->getController(); + } + + /** + * Constructor hook method. + * + * Implement this method to avoid having to overwrite + * the constructor and call parent. + * + * @param array $config The configuration settings provided to this component. + * @return void + */ + public function initialize(array $config) + { + } + + /** + * Magic method for lazy loading $components. + * + * @param string $name Name of component to get. + * @return mixed A Component object or null. + */ + public function __get($name) + { + if (isset($this->_componentMap[$name]) && !isset($this->{$name})) { + $config = (array)$this->_componentMap[$name]['config'] + ['enabled' => false]; + $this->{$name} = $this->_registry->load($this->_componentMap[$name]['class'], $config); + } + if (!isset($this->{$name})) { + return null; + } + + return $this->{$name}; + } + + /** + * Get the Controller callbacks this Component is interested in. + * + * Uses Conventions to map controller events to standard component + * callback method names. By defining one of the callback methods a + * component is assumed to be interested in the related event. + * + * Override this method if you need to add non-conventional event listeners. + * Or if you want components to listen to non-standard events. + * + * @return array + */ + public function implementedEvents() + { + $eventMap = [ + 'Controller.initialize' => 'beforeFilter', + 'Controller.startup' => 'startup', + 'Controller.beforeRender' => 'beforeRender', + 'Controller.beforeRedirect' => 'beforeRedirect', + 'Controller.shutdown' => 'shutdown', + ]; + $events = []; + foreach ($eventMap as $event => $method) { + if (method_exists($this, $method)) { + $events[$event] = $method; + } + } + + return $events; + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + return [ + 'components' => $this->components, + 'implementedEvents' => $this->implementedEvents(), + '_config' => $this->getConfig(), + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/Component/AuthComponent.php b/app/vendor/cakephp/cakephp/src/Controller/Component/AuthComponent.php new file mode 100644 index 000000000..23c9b71a3 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/Component/AuthComponent.php @@ -0,0 +1,1031 @@ +Auth->setConfig('authenticate', [ + * 'Form' => [ + * 'userModel' => 'Users.Users' + * ] + * ]); + * ``` + * + * Using the class name without 'Authenticate' as the key, you can pass in an + * array of config for each authentication object. Additionally you can define + * config that should be set to all authentications objects using the 'all' key: + * + * ``` + * $this->Auth->setConfig('authenticate', [ + * AuthComponent::ALL => [ + * 'userModel' => 'Users.Users', + * 'scope' => ['Users.active' => 1] + * ], + * 'Form', + * 'Basic' + * ]); + * ``` + * + * - `authorize` - An array of authorization objects to use for authorizing users. + * You can configure multiple adapters and they will be checked sequentially + * when authorization checks are done. + * + * ``` + * $this->Auth->setConfig('authorize', [ + * 'Crud' => [ + * 'actionPath' => 'controllers/' + * ] + * ]); + * ``` + * + * Using the class name without 'Authorize' as the key, you can pass in an array + * of config for each authorization object. Additionally you can define config + * that should be set to all authorization objects using the AuthComponent::ALL key: + * + * ``` + * $this->Auth->setConfig('authorize', [ + * AuthComponent::ALL => [ + * 'actionPath' => 'controllers/' + * ], + * 'Crud', + * 'CustomAuth' + * ]); + * ``` + * + * - ~~`ajaxLogin`~~ - The name of an optional view element to render when an Ajax + * request is made with an invalid or expired session. + * **This option is deprecated since 3.3.6.** Your client side code should + * instead check for 403 status code and show appropriate login form. + * + * - `flash` - Settings to use when Auth needs to do a flash message with + * FlashComponent::set(). Available keys are: + * + * - `key` - The message domain to use for flashes generated by this component, + * defaults to 'auth'. + * - `element` - Flash element to use, defaults to 'default'. + * - `params` - The array of additional params to use, defaults to ['class' => 'error'] + * + * - `loginAction` - A URL (defined as a string or array) to the controller action + * that handles logins. Defaults to `/users/login`. + * + * - `loginRedirect` - Normally, if a user is redirected to the `loginAction` page, + * the location they were redirected from will be stored in the session so that + * they can be redirected back after a successful login. If this session value + * is not set, redirectUrl() method will return the URL specified in `loginRedirect`. + * + * - `logoutRedirect` - The default action to redirect to after the user is logged out. + * While AuthComponent does not handle post-logout redirection, a redirect URL + * will be returned from `AuthComponent::logout()`. Defaults to `loginAction`. + * + * - `authError` - Error to display when user attempts to access an object or + * action to which they do not have access. + * + * - `unauthorizedRedirect` - Controls handling of unauthorized access. + * + * - For default value `true` unauthorized user is redirected to the referrer URL + * or `$loginRedirect` or '/'. + * - If set to a string or array the value is used as a URL to redirect to. + * - If set to false a `ForbiddenException` exception is thrown instead of redirecting. + * + * - `storage` - Storage class to use for persisting user record. When using + * stateless authenticator you should set this to 'Memory'. Defaults to 'Session'. + * + * - `checkAuthIn` - Name of event for which initial auth checks should be done. + * Defaults to 'Controller.startup'. You can set it to 'Controller.initialize' + * if you want the check to be done before controller's beforeFilter() is run. + * + * @var array + */ + protected $_defaultConfig = [ + 'authenticate' => null, + 'authorize' => null, + 'ajaxLogin' => null, + 'flash' => null, + 'loginAction' => null, + 'loginRedirect' => null, + 'logoutRedirect' => null, + 'authError' => null, + 'unauthorizedRedirect' => true, + 'storage' => 'Session', + 'checkAuthIn' => 'Controller.startup' + ]; + + /** + * Other components utilized by AuthComponent + * + * @var array + */ + public $components = ['RequestHandler', 'Flash']; + + /** + * Objects that will be used for authentication checks. + * + * @var \Cake\Auth\BaseAuthenticate[] + */ + protected $_authenticateObjects = []; + + /** + * Objects that will be used for authorization checks. + * + * @var \Cake\Auth\BaseAuthorize[] + */ + protected $_authorizeObjects = []; + + /** + * Storage object. + * + * @var \Cake\Auth\Storage\StorageInterface|null + */ + protected $_storage; + + /** + * Controller actions for which user validation is not required. + * + * @var array + * @see \Cake\Controller\Component\AuthComponent::allow() + */ + public $allowedActions = []; + + /** + * Request object + * + * @var \Cake\Http\ServerRequest + */ + public $request; + + /** + * Response object + * + * @var \Cake\Http\Response + */ + public $response; + + /** + * Instance of the Session object + * + * @var \Cake\Http\Session + * @deprecated 3.1.0 Will be removed in 4.0 + */ + public $session; + + /** + * The instance of the Authenticate provider that was used for + * successfully logging in the current user after calling `login()` + * in the same request + * + * @var \Cake\Auth\BaseAuthenticate + */ + protected $_authenticationProvider; + + /** + * The instance of the Authorize provider that was used to grant + * access to the current user to the URL they are requesting. + * + * @var \Cake\Auth\BaseAuthorize + */ + protected $_authorizationProvider; + + /** + * Initialize properties. + * + * @param array $config The config data. + * @return void + */ + public function initialize(array $config) + { + $controller = $this->_registry->getController(); + $this->setEventManager($controller->getEventManager()); + $this->response =& $controller->response; + $this->session = $controller->getRequest()->getSession(); + + if ($this->getConfig('ajaxLogin')) { + deprecationWarning( + 'The `ajaxLogin` option is deprecated. Your client-side ' . + 'code should instead check for 403 status code and show ' . + 'appropriate login form.' + ); + } + } + + /** + * Callback for Controller.startup event. + * + * @param \Cake\Event\Event $event Event instance. + * @return \Cake\Http\Response|null + */ + public function startup(Event $event) + { + return $this->authCheck($event); + } + + /** + * Main execution method, handles initial authentication check and redirection + * of invalid users. + * + * The auth check is done when event name is same as the one configured in + * `checkAuthIn` config. + * + * @param \Cake\Event\Event $event Event instance. + * @return \Cake\Http\Response|null + * @throws \ReflectionException + */ + public function authCheck(Event $event) + { + if ($this->_config['checkAuthIn'] !== $event->getName()) { + return null; + } + + /** @var \Cake\Controller\Controller $controller */ + $controller = $event->getSubject(); + + $action = strtolower($controller->getRequest()->getParam('action')); + if (!$controller->isAction($action)) { + return null; + } + + $this->_setDefaults(); + + if ($this->_isAllowed($controller)) { + return null; + } + + $isLoginAction = $this->_isLoginAction($controller); + + if (!$this->_getUser()) { + if ($isLoginAction) { + return null; + } + $result = $this->_unauthenticated($controller); + if ($result instanceof Response) { + $event->stopPropagation(); + } + + return $result; + } + + if ($isLoginAction || + empty($this->_config['authorize']) || + $this->isAuthorized($this->user()) + ) { + return null; + } + + $event->stopPropagation(); + + return $this->_unauthorized($controller); + } + + /** + * Events supported by this component. + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Controller.initialize' => 'authCheck', + 'Controller.startup' => 'startup', + ]; + } + + /** + * Checks whether current action is accessible without authentication. + * + * @param \Cake\Controller\Controller $controller A reference to the instantiating + * controller object + * @return bool True if action is accessible without authentication else false + */ + protected function _isAllowed(Controller $controller) + { + $action = strtolower($controller->getRequest()->getParam('action')); + + return in_array($action, array_map('strtolower', $this->allowedActions)); + } + + /** + * Handles unauthenticated access attempt. First the `unauthenticated()` method + * of the last authenticator in the chain will be called. The authenticator can + * handle sending response or redirection as appropriate and return `true` to + * indicate no further action is necessary. If authenticator returns null this + * method redirects user to login action. If it's an AJAX request and config + * `ajaxLogin` is specified that element is rendered else a 403 HTTP status code + * is returned. + * + * @param \Cake\Controller\Controller $controller A reference to the controller object. + * @return \Cake\Http\Response|null Null if current action is login action + * else response object returned by authenticate object or Controller::redirect(). + * @throws \Cake\Core\Exception\Exception + */ + protected function _unauthenticated(Controller $controller) + { + if (empty($this->_authenticateObjects)) { + $this->constructAuthenticate(); + } + $response = $this->response; + $auth = end($this->_authenticateObjects); + if ($auth === false) { + throw new Exception('At least one authenticate object must be available.'); + } + $result = $auth->unauthenticated($controller->getRequest(), $response); + if ($result !== null) { + return $result; + } + + if (!$controller->getRequest()->is('ajax')) { + $this->flash($this->_config['authError']); + + return $controller->redirect($this->_loginActionRedirectUrl()); + } + + if (!empty($this->_config['ajaxLogin'])) { + $controller->viewBuilder()->setTemplatePath('Element'); + $response = $controller->render( + $this->_config['ajaxLogin'], + $this->RequestHandler->ajaxLayout + ); + } + + return $response->withStatus(403); + } + + /** + * Returns the URL of the login action to redirect to. + * + * This includes the redirect query string if applicable. + * + * @return array|string + */ + protected function _loginActionRedirectUrl() + { + $urlToRedirectBackTo = $this->_getUrlToRedirectBackTo(); + + $loginAction = $this->_config['loginAction']; + if ($urlToRedirectBackTo === '/') { + return $loginAction; + } + + if (is_array($loginAction)) { + $loginAction['?'][static::QUERY_STRING_REDIRECT] = $urlToRedirectBackTo; + } else { + $char = strpos($loginAction, '?') === false ? '?' : '&'; + $loginAction .= $char . static::QUERY_STRING_REDIRECT . '=' . urlencode($urlToRedirectBackTo); + } + + return $loginAction; + } + + /** + * Normalizes config `loginAction` and checks if current request URL is same as login action. + * + * @param \Cake\Controller\Controller $controller A reference to the controller object. + * @return bool True if current action is login action else false. + */ + protected function _isLoginAction(Controller $controller) + { + $uri = $controller->request->getUri(); + $url = Router::normalize($uri->getPath()); + $loginAction = Router::normalize($this->_config['loginAction']); + + return $loginAction === $url; + } + + /** + * Handle unauthorized access attempt + * + * @param \Cake\Controller\Controller $controller A reference to the controller object + * @return \Cake\Http\Response + * @throws \Cake\Http\Exception\ForbiddenException + */ + protected function _unauthorized(Controller $controller) + { + if ($this->_config['unauthorizedRedirect'] === false) { + throw new ForbiddenException($this->_config['authError']); + } + + $this->flash($this->_config['authError']); + if ($this->_config['unauthorizedRedirect'] === true) { + $default = '/'; + if (!empty($this->_config['loginRedirect'])) { + $default = $this->_config['loginRedirect']; + } + if (is_array($default)) { + $default['_base'] = false; + } + $url = $controller->referer($default, true); + } else { + $url = $this->_config['unauthorizedRedirect']; + } + + return $controller->redirect($url); + } + + /** + * Sets defaults for configs. + * + * @return void + */ + protected function _setDefaults() + { + $defaults = [ + 'authenticate' => ['Form'], + 'flash' => [ + 'element' => 'error', + 'key' => 'flash', + 'params' => ['class' => 'error'] + ], + 'loginAction' => [ + 'controller' => 'Users', + 'action' => 'login', + 'plugin' => null + ], + 'logoutRedirect' => $this->_config['loginAction'], + 'authError' => __d('cake', 'You are not authorized to access that location.') + ]; + + $config = $this->getConfig(); + foreach ($config as $key => $value) { + if ($value !== null) { + unset($defaults[$key]); + } + } + $this->setConfig($defaults); + } + + /** + * Check if the provided user is authorized for the request. + * + * Uses the configured Authorization adapters to check whether or not a user is authorized. + * Each adapter will be checked in sequence, if any of them return true, then the user will + * be authorized for the request. + * + * @param array|\ArrayAccess|null $user The user to check the authorization of. + * If empty the user fetched from storage will be used. + * @param \Cake\Http\ServerRequest|null $request The request to authenticate for. + * If empty, the current request will be used. + * @return bool True if $user is authorized, otherwise false + */ + public function isAuthorized($user = null, ServerRequest $request = null) + { + if (empty($user) && !$this->user()) { + return false; + } + if (empty($user)) { + $user = $this->user(); + } + if (empty($request)) { + $request = $this->getController()->getRequest(); + } + if (empty($this->_authorizeObjects)) { + $this->constructAuthorize(); + } + foreach ($this->_authorizeObjects as $authorizer) { + if ($authorizer->authorize($user, $request) === true) { + $this->_authorizationProvider = $authorizer; + + return true; + } + } + + return false; + } + + /** + * Loads the authorization objects configured. + * + * @return array|null The loaded authorization objects, or null when authorize is empty. + * @throws \Cake\Core\Exception\Exception + */ + public function constructAuthorize() + { + if (empty($this->_config['authorize'])) { + return null; + } + $this->_authorizeObjects = []; + $authorize = Hash::normalize((array)$this->_config['authorize']); + $global = []; + if (isset($authorize[AuthComponent::ALL])) { + $global = $authorize[AuthComponent::ALL]; + unset($authorize[AuthComponent::ALL]); + } + foreach ($authorize as $alias => $config) { + if (!empty($config['className'])) { + $class = $config['className']; + unset($config['className']); + } else { + $class = $alias; + } + $className = App::className($class, 'Auth', 'Authorize'); + if (!class_exists($className)) { + throw new Exception(sprintf('Authorization adapter "%s" was not found.', $class)); + } + if (!method_exists($className, 'authorize')) { + throw new Exception('Authorization objects must implement an authorize() method.'); + } + $config = (array)$config + $global; + $this->_authorizeObjects[$alias] = new $className($this->_registry, $config); + } + + return $this->_authorizeObjects; + } + + /** + * Getter for authorize objects. Will return a particular authorize object. + * + * @param string $alias Alias for the authorize object + * @return \Cake\Auth\BaseAuthorize|null + */ + public function getAuthorize($alias) + { + if (empty($this->_authorizeObjects)) { + $this->constructAuthorize(); + } + + return isset($this->_authorizeObjects[$alias]) ? $this->_authorizeObjects[$alias] : null; + } + + /** + * Takes a list of actions in the current controller for which authentication is not required, or + * no parameters to allow all actions. + * + * You can use allow with either an array or a simple string. + * + * ``` + * $this->Auth->allow('view'); + * $this->Auth->allow(['edit', 'add']); + * ``` + * or to allow all actions + * ``` + * $this->Auth->allow(); + * ``` + * + * @param string|array|null $actions Controller action name or array of actions + * @return void + * @link https://book.cakephp.org/3.0/en/controllers/components/authentication.html#making-actions-public + */ + public function allow($actions = null) + { + if ($actions === null) { + $controller = $this->_registry->getController(); + $this->allowedActions = get_class_methods($controller); + + return; + } + $this->allowedActions = array_merge($this->allowedActions, (array)$actions); + } + + /** + * Removes items from the list of allowed/no authentication required actions. + * + * You can use deny with either an array or a simple string. + * + * ``` + * $this->Auth->deny('view'); + * $this->Auth->deny(['edit', 'add']); + * ``` + * or + * ``` + * $this->Auth->deny(); + * ``` + * to remove all items from the allowed list + * + * @param string|array|null $actions Controller action name or array of actions + * @return void + * @see \Cake\Controller\Component\AuthComponent::allow() + * @link https://book.cakephp.org/3.0/en/controllers/components/authentication.html#making-actions-require-authorization + */ + public function deny($actions = null) + { + if ($actions === null) { + $this->allowedActions = []; + + return; + } + foreach ((array)$actions as $action) { + $i = array_search($action, $this->allowedActions); + if (is_int($i)) { + unset($this->allowedActions[$i]); + } + } + $this->allowedActions = array_values($this->allowedActions); + } + + /** + * Set provided user info to storage as logged in user. + * + * The storage class is configured using `storage` config key or passing + * instance to AuthComponent::storage(). + * + * @param array|\ArrayAccess $user User data. + * @return void + * @link https://book.cakephp.org/3.0/en/controllers/components/authentication.html#identifying-users-and-logging-them-in + */ + public function setUser($user) + { + $this->storage()->write($user); + } + + /** + * Log a user out. + * + * Returns the logout action to redirect to. Triggers the `Auth.logout` event + * which the authenticate classes can listen for and perform custom logout logic. + * + * @return string Normalized config `logoutRedirect` + * @link https://book.cakephp.org/3.0/en/controllers/components/authentication.html#logging-users-out + */ + public function logout() + { + $this->_setDefaults(); + if (empty($this->_authenticateObjects)) { + $this->constructAuthenticate(); + } + $user = (array)$this->user(); + $this->dispatchEvent('Auth.logout', [$user]); + $this->storage()->delete(); + + return Router::normalize($this->_config['logoutRedirect']); + } + + /** + * Get the current user from storage. + * + * @param string|null $key Field to retrieve. Leave null to get entire User record. + * @return mixed|null Either User record or null if no user is logged in, or retrieved field if key is specified. + * @link https://book.cakephp.org/3.0/en/controllers/components/authentication.html#accessing-the-logged-in-user + */ + public function user($key = null) + { + $user = $this->storage()->read(); + if (!$user) { + return null; + } + + if ($key === null) { + return $user; + } + + return Hash::get($user, $key); + } + + /** + * Similar to AuthComponent::user() except if user is not found in + * configured storage, connected authentication objects will have their + * getUser() methods called. + * + * This lets stateless authentication methods function correctly. + * + * @return bool true If a user can be found, false if one cannot. + */ + protected function _getUser() + { + $user = $this->user(); + if ($user) { + return true; + } + + if (empty($this->_authenticateObjects)) { + $this->constructAuthenticate(); + } + foreach ($this->_authenticateObjects as $auth) { + $result = $auth->getUser($this->getController()->getRequest()); + if (!empty($result) && is_array($result)) { + $this->_authenticationProvider = $auth; + $event = $this->dispatchEvent('Auth.afterIdentify', [$result, $auth]); + if ($event->getResult() !== null) { + $result = $event->getResult(); + } + $this->storage()->write($result); + + return true; + } + } + + return false; + } + + /** + * Get the URL a user should be redirected to upon login. + * + * Pass a URL in to set the destination a user should be redirected to upon + * logging in. + * + * If no parameter is passed, gets the authentication redirect URL. The URL + * returned is as per following rules: + * + * - Returns the normalized redirect URL from storage if it is + * present and for the same domain the current app is running on. + * - If there is no URL returned from storage and there is a config + * `loginRedirect`, the `loginRedirect` value is returned. + * - If there is no session and no `loginRedirect`, / is returned. + * + * @param string|array|null $url Optional URL to write as the login redirect URL. + * @return string Redirect URL + */ + public function redirectUrl($url = null) + { + $redirectUrl = $this->getController()->getRequest()->getQuery(static::QUERY_STRING_REDIRECT); + if ($redirectUrl && (substr($redirectUrl, 0, 1) !== '/' || substr($redirectUrl, 0, 2) === '//')) { + $redirectUrl = null; + } + + if ($url !== null) { + $redirectUrl = $url; + } elseif ($redirectUrl) { + if (Router::normalize($redirectUrl) === Router::normalize($this->_config['loginAction'])) { + $redirectUrl = $this->_config['loginRedirect']; + } + } elseif ($this->_config['loginRedirect']) { + $redirectUrl = $this->_config['loginRedirect']; + } else { + $redirectUrl = '/'; + } + if (is_array($redirectUrl)) { + return Router::url($redirectUrl + ['_base' => false]); + } + + return $redirectUrl; + } + + /** + * Use the configured authentication adapters, and attempt to identify the user + * by credentials contained in $request. + * + * Triggers `Auth.afterIdentify` event which the authenticate classes can listen + * to. + * + * @return array|bool User record data, or false, if the user could not be identified. + */ + public function identify() + { + $this->_setDefaults(); + + if (empty($this->_authenticateObjects)) { + $this->constructAuthenticate(); + } + foreach ($this->_authenticateObjects as $auth) { + $result = $auth->authenticate($this->getController()->getRequest(), $this->response); + if (!empty($result)) { + $this->_authenticationProvider = $auth; + $event = $this->dispatchEvent('Auth.afterIdentify', [$result, $auth]); + if ($event->getResult() !== null) { + return $event->getResult(); + } + + return $result; + } + } + + return false; + } + + /** + * Loads the configured authentication objects. + * + * @return array|null The loaded authorization objects, or null on empty authenticate value. + * @throws \Cake\Core\Exception\Exception + */ + public function constructAuthenticate() + { + if (empty($this->_config['authenticate'])) { + return null; + } + $this->_authenticateObjects = []; + $authenticate = Hash::normalize((array)$this->_config['authenticate']); + $global = []; + if (isset($authenticate[AuthComponent::ALL])) { + $global = $authenticate[AuthComponent::ALL]; + unset($authenticate[AuthComponent::ALL]); + } + foreach ($authenticate as $alias => $config) { + if (!empty($config['className'])) { + $class = $config['className']; + unset($config['className']); + } else { + $class = $alias; + } + $className = App::className($class, 'Auth', 'Authenticate'); + if (!class_exists($className)) { + throw new Exception(sprintf('Authentication adapter "%s" was not found.', $class)); + } + if (!method_exists($className, 'authenticate')) { + throw new Exception('Authentication objects must implement an authenticate() method.'); + } + $config = array_merge($global, (array)$config); + $this->_authenticateObjects[$alias] = new $className($this->_registry, $config); + $this->getEventManager()->on($this->_authenticateObjects[$alias]); + } + + return $this->_authenticateObjects; + } + + /** + * Get/set user record storage object. + * + * @param \Cake\Auth\Storage\StorageInterface|null $storage Sets provided + * object as storage or if null returns configured storage object. + * @return \Cake\Auth\Storage\StorageInterface|\Cake\Core\InstanceConfigTrait|null + */ + public function storage(StorageInterface $storage = null) + { + if ($storage !== null) { + $this->_storage = $storage; + + return null; + } + + if ($this->_storage) { + return $this->_storage; + } + + $config = $this->_config['storage']; + if (is_string($config)) { + $class = $config; + $config = []; + } else { + $class = $config['className']; + unset($config['className']); + } + $className = App::className($class, 'Auth/Storage', 'Storage'); + if (!class_exists($className)) { + throw new Exception(sprintf('Auth storage adapter "%s" was not found.', $class)); + } + $request = $this->getController()->getRequest(); + $response = $this->getController()->getResponse(); + $this->_storage = new $className($request, $response, $config); + + return $this->_storage; + } + + /** + * Magic accessor for backward compatibility for property `$sessionKey`. + * + * @param string $name Property name + * @return mixed + */ + public function __get($name) + { + if ($name === 'sessionKey') { + return $this->storage()->getConfig('key'); + } + + return parent::__get($name); + } + + /** + * Magic setter for backward compatibility for property `$sessionKey`. + * + * @param string $name Property name. + * @param mixed $value Value to set. + * @return void + */ + public function __set($name, $value) + { + if ($name === 'sessionKey') { + $this->_storage = null; + + if ($value === false) { + $this->setConfig('storage', 'Memory'); + + return; + } + + $this->setConfig('storage', 'Session'); + $this->storage()->setConfig('key', $value); + + return; + } + + $this->{$name} = $value; + } + + /** + * Getter for authenticate objects. Will return a particular authenticate object. + * + * @param string $alias Alias for the authenticate object + * + * @return \Cake\Auth\BaseAuthenticate|null + */ + public function getAuthenticate($alias) + { + if (empty($this->_authenticateObjects)) { + $this->constructAuthenticate(); + } + + return isset($this->_authenticateObjects[$alias]) ? $this->_authenticateObjects[$alias] : null; + } + + /** + * Set a flash message. Uses the Flash component with values from `flash` config. + * + * @param string $message The message to set. + * @return void + */ + public function flash($message) + { + if ($message === false) { + return; + } + + $this->Flash->set($message, $this->_config['flash']); + } + + /** + * If login was called during this request and the user was successfully + * authenticated, this function will return the instance of the authentication + * object that was used for logging the user in. + * + * @return \Cake\Auth\BaseAuthenticate|null + */ + public function authenticationProvider() + { + return $this->_authenticationProvider; + } + + /** + * If there was any authorization processing for the current request, this function + * will return the instance of the Authorization object that granted access to the + * user to the current address. + * + * @return \Cake\Auth\BaseAuthorize|null + */ + public function authorizationProvider() + { + return $this->_authorizationProvider; + } + + /** + * Returns the URL to redirect back to or / if not possible. + * + * This method takes the referrer into account if the + * request is not of type GET. + * + * @return string + */ + protected function _getUrlToRedirectBackTo() + { + $urlToRedirectBackTo = $this->request->getRequestTarget(); + if (!$this->request->is('get')) { + $urlToRedirectBackTo = $this->request->referer(true); + } + + return $urlToRedirectBackTo; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/Component/CookieComponent.php b/app/vendor/cakephp/cakephp/src/Controller/Component/CookieComponent.php new file mode 100644 index 000000000..7cf09be55 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/Component/CookieComponent.php @@ -0,0 +1,357 @@ + null, + 'domain' => '', + 'secure' => false, + 'key' => null, + 'httpOnly' => false, + 'encryption' => 'aes', + 'expires' => '+1 month', + ]; + + /** + * Config specific to a given top level key name. + * + * The values in this array are merged with the general config + * to generate the configuration for a given top level cookie name. + * + * @var array + */ + protected $_keyConfig = []; + + /** + * Values stored in the cookie. + * + * Accessed in the controller using $this->Cookie->read('Name.key'); + * + * @var array + */ + protected $_values = []; + + /** + * A map of keys that have been loaded. + * + * Since CookieComponent lazily reads cookie data, + * we need to track which cookies have been read to account for + * read, delete, read patterns. + * + * @var array + */ + protected $_loaded = []; + + /** + * A reference to the Controller's Cake\Http\Response object. + * Currently unused. + * + * @var \Cake\Http\Response|null + * @deprecated 3.4.0 Will be removed in 4.0.0 + */ + protected $_response; + + /** + * Initialize config data and properties. + * + * @param array $config The config data. + * @return void + */ + public function initialize(array $config) + { + if (!$this->_config['key']) { + $this->setConfig('key', Security::getSalt()); + } + + $controller = $this->_registry->getController(); + + if ($controller === null) { + $this->request = ServerRequestFactory::fromGlobals(); + } + + if (empty($this->_config['path'])) { + $this->setConfig('path', $this->getController()->getRequest()->getAttribute('webroot')); + } + } + + /** + * Set the configuration for a specific top level key. + * + * ### Examples: + * + * Set a single config option for a key: + * + * ``` + * $this->Cookie->configKey('User', 'expires', '+3 months'); + * ``` + * + * Set multiple options: + * + * ``` + * $this->Cookie->configKey('User', [ + * 'expires', '+3 months', + * 'httpOnly' => true, + * ]); + * ``` + * + * @param string $keyname The top level keyname to configure. + * @param null|string|array $option Either the option name to set, or an array of options to set, + * or null to read config options for a given key. + * @param string|null $value Either the value to set, or empty when $option is an array. + * @return array|null + */ + public function configKey($keyname, $option = null, $value = null) + { + if ($option === null) { + $default = $this->_config; + $local = isset($this->_keyConfig[$keyname]) ? $this->_keyConfig[$keyname] : []; + + return $local + $default; + } + if (!is_array($option)) { + $option = [$option => $value]; + } + $this->_keyConfig[$keyname] = $option; + + return null; + } + + /** + * Events supported by this component. + * + * @return array + */ + public function implementedEvents() + { + return []; + } + + /** + * Write a value to the response cookies. + * + * You must use this method before any output is sent to the browser. + * Failure to do so will result in header already sent errors. + * + * @param string|array $key Key for the value + * @param mixed $value Value + * @return void + */ + public function write($key, $value = null) + { + if (!is_array($key)) { + $key = [$key => $value]; + } + + $keys = []; + foreach ($key as $name => $value) { + $this->_load($name); + + $this->_values = Hash::insert($this->_values, $name, $value); + $parts = explode('.', $name); + $keys[] = $parts[0]; + } + + foreach ($keys as $name) { + $this->_write($name, $this->_values[$name]); + } + } + + /** + * Read the value of key path from request cookies. + * + * This method will also allow you to read cookies that have been written in this + * request, but not yet sent to the client. + * + * @param string|null $key Key of the value to be obtained. + * @return string or null, value for specified key + */ + public function read($key = null) + { + $this->_load($key); + + return Hash::get($this->_values, $key); + } + + /** + * Load the cookie data from the request and response objects. + * + * Based on the configuration data, cookies will be decrypted. When cookies + * contain array data, that data will be expanded. + * + * @param string|array $key The key to load. + * @return void + */ + protected function _load($key) + { + $parts = explode('.', $key); + $first = array_shift($parts); + if (isset($this->_loaded[$first])) { + return; + } + $cookie = $this->getController()->getRequest()->getCookie($first); + if ($cookie === null) { + return; + } + $config = $this->configKey($first); + $this->_loaded[$first] = true; + $this->_values[$first] = $this->_decrypt($cookie, $config['encryption'], $config['key']); + } + + /** + * Returns true if given key is set in the cookie. + * + * @param string|null $key Key to check for + * @return bool True if the key exists + */ + public function check($key = null) + { + if (empty($key)) { + return false; + } + + return $this->read($key) !== null; + } + + /** + * Delete a cookie value + * + * You must use this method before any output is sent to the browser. + * Failure to do so will result in header already sent errors. + * + * Deleting a top level key will delete all keys nested within that key. + * For example deleting the `User` key, will also delete `User.email`. + * + * @param string $key Key of the value to be deleted + * @return void + */ + public function delete($key) + { + $this->_load($key); + + $this->_values = Hash::remove($this->_values, $key); + $parts = explode('.', $key); + $top = $parts[0]; + + if (isset($this->_values[$top])) { + $this->_write($top, $this->_values[$top]); + } else { + $this->_delete($top); + } + } + + /** + * Set cookie + * + * @param string $name Name for cookie + * @param string $value Value for cookie + * @return void + */ + protected function _write($name, $value) + { + $config = $this->configKey($name); + $expires = new Time($config['expires']); + + $controller = $this->getController(); + $controller->response = $controller->response->withCookie($name, [ + 'value' => $this->_encrypt($value, $config['encryption'], $config['key']), + 'expire' => $expires->format('U'), + 'path' => $config['path'], + 'domain' => $config['domain'], + 'secure' => (bool)$config['secure'], + 'httpOnly' => (bool)$config['httpOnly'] + ]); + } + + /** + * Sets a cookie expire time to remove cookie value. + * + * This is only done once all values in a cookie key have been + * removed with delete. + * + * @param string $name Name of cookie + * @return void + */ + protected function _delete($name) + { + $config = $this->configKey($name); + $expires = new Time('now'); + $controller = $this->getController(); + + $controller->response = $controller->response->withCookie($name, [ + 'value' => '', + 'expire' => $expires->format('U') - 42000, + 'path' => $config['path'], + 'domain' => $config['domain'], + 'secure' => $config['secure'], + 'httpOnly' => $config['httpOnly'] + ]); + } + + /** + * Returns the encryption key to be used. + * + * @return string + */ + protected function _getCookieEncryptionKey() + { + return $this->_config['key']; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/Component/CsrfComponent.php b/app/vendor/cakephp/cakephp/src/Controller/Component/CsrfComponent.php new file mode 100644 index 000000000..e09c8a918 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/Component/CsrfComponent.php @@ -0,0 +1,169 @@ +Form->create(...)` is used in a view. + * + * @deprecated 3.5.0 Use Cake\Http\Middleware\CsrfProtectionMiddleware instead. + */ +class CsrfComponent extends Component +{ + + /** + * Default config for the CSRF handling. + * + * - cookieName = The name of the cookie to send. + * - expiry = How long the CSRF token should last. Defaults to browser session. + * - secure = Whether or not the cookie will be set with the Secure flag. Defaults to false. + * - httpOnly = Whether or not the cookie will be set with the HttpOnly flag. Defaults to false. + * - field = The form field to check. Changing this will also require configuring + * FormHelper. + * + * @var array + */ + protected $_defaultConfig = [ + 'cookieName' => 'csrfToken', + 'expiry' => 0, + 'secure' => false, + 'httpOnly' => false, + 'field' => '_csrfToken', + ]; + + /** + * Startup callback. + * + * Validates the CSRF token for POST data. If + * the request is a GET request, and the cookie value is absent a cookie will be set. + * + * Once a cookie is set it will be copied into request->getParam('_csrfToken') + * so that application and framework code can easily access the csrf token. + * + * RequestAction requests do not get checked, nor will + * they set a cookie should it be missing. + * + * @param \Cake\Event\Event $event Event instance. + * @return void + */ + public function startup(Event $event) + { + /** @var \Cake\Controller\Controller $controller */ + $controller = $event->getSubject(); + $request = $controller->getRequest(); + $response = $controller->getResponse(); + $cookieName = $this->_config['cookieName']; + + $cookieData = $request->getCookie($cookieName); + if ($cookieData) { + $request = $request->withParam('_csrfToken', $cookieData); + } + + if ($request->is('requested')) { + $controller->setRequest($request); + + return; + } + + if ($request->is('get') && $cookieData === null) { + list($request, $response) = $this->_setCookie($request, $response); + $controller->setResponse($response); + } + if ($request->is(['put', 'post', 'delete', 'patch']) || $request->getData()) { + $this->_validateToken($request); + $request = $request->withoutData($this->_config['field']); + } + $controller->setRequest($request); + } + + /** + * Events supported by this component. + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Controller.startup' => 'startup', + ]; + } + + /** + * Set the cookie in the response. + * + * Also sets the request->params['_csrfToken'] so the newly minted + * token is available in the request data. + * + * @param \Cake\Http\ServerRequest $request The request object. + * @param \Cake\Http\Response $response The response object. + * @return array An array of the modified request, response. + */ + protected function _setCookie(ServerRequest $request, Response $response) + { + $expiry = new Time($this->_config['expiry']); + $value = hash('sha512', Security::randomBytes(16), false); + + $request = $request->withParam('_csrfToken', $value); + $response = $response->withCookie($this->_config['cookieName'], [ + 'value' => $value, + 'expire' => $expiry->format('U'), + 'path' => $request->getAttribute('webroot'), + 'secure' => $this->_config['secure'], + 'httpOnly' => $this->_config['httpOnly'], + ]); + + return [$request, $response]; + } + + /** + * Validate the request data against the cookie token. + * + * @param \Cake\Http\ServerRequest $request The request to validate against. + * @throws \Cake\Http\Exception\InvalidCsrfTokenException when the CSRF token is invalid or missing. + * @return void + */ + protected function _validateToken(ServerRequest $request) + { + $cookie = $request->getCookie($this->_config['cookieName']); + $post = $request->getData($this->_config['field']); + $header = $request->getHeaderLine('X-CSRF-Token'); + + if (!$cookie) { + throw new InvalidCsrfTokenException(__d('cake', 'Missing CSRF token cookie')); + } + + if (!Security::constantEquals($post, $cookie) && !Security::constantEquals($header, $cookie)) { + throw new InvalidCsrfTokenException(__d('cake', 'CSRF token mismatch.')); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/Component/FlashComponent.php b/app/vendor/cakephp/cakephp/src/Controller/Component/FlashComponent.php new file mode 100644 index 000000000..e05d868d6 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/Component/FlashComponent.php @@ -0,0 +1,175 @@ + 'flash', + 'element' => 'default', + 'params' => [], + 'clear' => false, + 'duplicate' => true + ]; + + /** + * Constructor + * + * @param \Cake\Controller\ComponentRegistry $registry A ComponentRegistry for this component + * @param array $config Array of config. + */ + public function __construct(ComponentRegistry $registry, array $config = []) + { + parent::__construct($registry, $config); + $this->_session = $registry->getController()->getRequest()->getSession(); + } + + /** + * Used to set a session variable that can be used to output messages in the view. + * If you make consecutive calls to this method, the messages will stack (if they are + * set with the same flash key) + * + * In your controller: $this->Flash->set('This has been saved'); + * + * ### Options: + * + * - `key` The key to set under the session's Flash key + * - `element` The element used to render the flash message. Default to 'default'. + * - `params` An array of variables to make available when using an element + * - `clear` A bool stating if the current stack should be cleared to start a new one + * - `escape` Set to false to allow templates to print out HTML content + * + * @param string|\Exception $message Message to be flashed. If an instance + * of \Exception the exception message will be used and code will be set + * in params. + * @param array $options An array of options + * @return void + */ + public function set($message, array $options = []) + { + $options += $this->getConfig(); + + if ($message instanceof Exception) { + if (!isset($options['params']['code'])) { + $options['params']['code'] = $message->getCode(); + } + $message = $message->getMessage(); + } + + if (isset($options['escape']) && !isset($options['params']['escape'])) { + $options['params']['escape'] = $options['escape']; + } + + list($plugin, $element) = pluginSplit($options['element']); + + if ($plugin) { + $options['element'] = $plugin . '.Flash/' . $element; + } else { + $options['element'] = 'Flash/' . $element; + } + + $messages = []; + if (!$options['clear']) { + $messages = (array)$this->_session->read('Flash.' . $options['key']); + } + + if (!$options['duplicate']) { + foreach ($messages as $existingMessage) { + if ($existingMessage['message'] === $message) { + return; + } + } + } + + $messages[] = [ + 'message' => $message, + 'key' => $options['key'], + 'element' => $options['element'], + 'params' => $options['params'] + ]; + + $this->_session->write('Flash.' . $options['key'], $messages); + } + + /** + * Magic method for verbose flash methods based on element names. + * + * For example: $this->Flash->success('My message') would use the + * success.ctp element under `src/Template/Element/Flash` for rendering the + * flash message. + * + * If you make consecutive calls to this method, the messages will stack (if they are + * set with the same flash key) + * + * Note that the parameter `element` will be always overridden. In order to call a + * specific element from a plugin, you should set the `plugin` option in $args. + * + * For example: `$this->Flash->warning('My message', ['plugin' => 'PluginName'])` would + * use the warning.ctp element under `plugins/PluginName/src/Template/Element/Flash` for + * rendering the flash message. + * + * @param string $name Element name to use. + * @param array $args Parameters to pass when calling `FlashComponent::set()`. + * @return void + * @throws \Cake\Http\Exception\InternalErrorException If missing the flash message. + */ + public function __call($name, $args) + { + $element = Inflector::underscore($name); + + if (count($args) < 1) { + throw new InternalErrorException('Flash message missing.'); + } + + $options = ['element' => $element]; + + if (!empty($args[1])) { + if (!empty($args[1]['plugin'])) { + $options = ['element' => $args[1]['plugin'] . '.' . $element]; + unset($args[1]['plugin']); + } + $options += (array)$args[1]; + } + + $this->set($args[0], $options); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/Component/PaginatorComponent.php b/app/vendor/cakephp/cakephp/src/Controller/Component/PaginatorComponent.php new file mode 100644 index 000000000..e44a36bc5 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/Component/PaginatorComponent.php @@ -0,0 +1,348 @@ + 1, + 'limit' => 20, + 'maxLimit' => 100, + 'whitelist' => ['limit', 'sort', 'page', 'direction'] + ]; + + /** + * Datasource paginator instance. + * + * @var \Cake\Datasource\Paginator + */ + protected $_paginator; + + /** + * {@inheritDoc} + */ + public function __construct(ComponentRegistry $registry, array $config = []) + { + if (isset($config['paginator'])) { + if (!$config['paginator'] instanceof Paginator) { + throw new InvalidArgumentException('Paginator must be an instance of ' . Paginator::class); + } + $this->_paginator = $config['paginator']; + unset($config['paginator']); + } else { + $this->_paginator = new Paginator(); + } + + parent::__construct($registry, $config); + } + + /** + * Events supported by this component. + * + * @return array + */ + public function implementedEvents() + { + return []; + } + + /** + * Handles automatic pagination of model records. + * + * ### Configuring pagination + * + * When calling `paginate()` you can use the $settings parameter to pass in pagination settings. + * These settings are used to build the queries made and control other pagination settings. + * + * If your settings contain a key with the current table's alias. The data inside that key will be used. + * Otherwise the top level configuration will be used. + * + * ``` + * $settings = [ + * 'limit' => 20, + * 'maxLimit' => 100 + * ]; + * $results = $paginator->paginate($table, $settings); + * ``` + * + * The above settings will be used to paginate any Table. You can configure Table specific settings by + * keying the settings with the Table alias. + * + * ``` + * $settings = [ + * 'Articles' => [ + * 'limit' => 20, + * 'maxLimit' => 100 + * ], + * 'Comments' => [ ... ] + * ]; + * $results = $paginator->paginate($table, $settings); + * ``` + * + * This would allow you to have different pagination settings for `Articles` and `Comments` tables. + * + * ### Controlling sort fields + * + * By default CakePHP will automatically allow sorting on any column on the table object being + * paginated. Often times you will want to allow sorting on either associated columns or calculated + * fields. In these cases you will need to define a whitelist of all the columns you wish to allow + * sorting on. You can define the whitelist in the `$settings` parameter: + * + * ``` + * $settings = [ + * 'Articles' => [ + * 'finder' => 'custom', + * 'sortWhitelist' => ['title', 'author_id', 'comment_count'], + * ] + * ]; + * ``` + * + * Passing an empty array as whitelist disallows sorting altogether. + * + * ### Paginating with custom finders + * + * You can paginate with any find type defined on your table using the `finder` option. + * + * ``` + * $settings = [ + * 'Articles' => [ + * 'finder' => 'popular' + * ] + * ]; + * $results = $paginator->paginate($table, $settings); + * ``` + * + * Would paginate using the `find('popular')` method. + * + * You can also pass an already created instance of a query to this method: + * + * ``` + * $query = $this->Articles->find('popular')->matching('Tags', function ($q) { + * return $q->where(['name' => 'CakePHP']) + * }); + * $results = $paginator->paginate($query); + * ``` + * + * ### Scoping Request parameters + * + * By using request parameter scopes you can paginate multiple queries in the same controller action: + * + * ``` + * $articles = $paginator->paginate($articlesQuery, ['scope' => 'articles']); + * $tags = $paginator->paginate($tagsQuery, ['scope' => 'tags']); + * ``` + * + * Each of the above queries will use different query string parameter sets + * for pagination data. An example URL paginating both results would be: + * + * ``` + * /dashboard?articles[page]=1&tags[page]=2 + * ``` + * + * @param \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface $object The table or query to paginate. + * @param array $settings The settings/configuration used for pagination. + * @return \Cake\Datasource\ResultSetInterface Query results + * @throws \Cake\Http\Exception\NotFoundException + */ + public function paginate($object, array $settings = []) + { + $request = $this->_registry->getController()->getRequest(); + + try { + $results = $this->_paginator->paginate( + $object, + $request->getQueryParams(), + $settings + ); + + $this->_setPagingParams(); + } catch (PageOutOfBoundsException $e) { + $this->_setPagingParams(); + + throw new NotFoundException(null, null, $e); + } + + return $results; + } + + /** + * Merges the various options that Pagination uses. + * Pulls settings together from the following places: + * + * - General pagination settings + * - Model specific settings. + * - Request parameters + * + * The result of this method is the aggregate of all the option sets combined together. You can change + * config value `whitelist` to modify which options/values can be set using request parameters. + * + * @param string $alias Model alias being paginated, if the general settings has a key with this value + * that key's settings will be used for pagination instead of the general ones. + * @param array $settings The settings to merge with the request data. + * @return array Array of merged options. + */ + public function mergeOptions($alias, $settings) + { + $request = $this->_registry->getController()->getRequest(); + + return $this->_paginator->mergeOptions( + $request->getQueryParams(), + $this->_paginator->getDefaults($alias, $settings) + ); + } + + /** + * Set paginator instance. + * + * @param \Cake\Datasource\Paginator $paginator Paginator instance. + * @return self + */ + public function setPaginator(Paginator $paginator) + { + $this->_paginator = $paginator; + + return $this; + } + + /** + * Get paginator instance. + * + * @return \Cake\Datasource\Paginator + */ + public function getPaginator() + { + return $this->_paginator; + } + + /** + * Set paging params to request instance. + * + * @return void + */ + protected function _setPagingParams() + { + $controller = $this->getController(); + $request = $controller->getRequest(); + $paging = $this->_paginator->getPagingParams() + (array)$request->getParam('paging', []); + + $controller->setRequest($request->withParam('paging', $paging)); + } + + /** + * Proxy getting/setting config options to Paginator. + * + * @deprecated 3.5.0 use setConfig()/getConfig() instead. + * @param string|array|null $key The key to get/set, or a complete array of configs. + * @param mixed|null $value The value to set. + * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true. + * @return mixed Config value being read, or the object itself on write operations. + */ + public function config($key = null, $value = null, $merge = true) + { + deprecationWarning('PaginatorComponent::config() is deprecated. Use getConfig()/setConfig() instead.'); + $return = $this->_paginator->config($key, $value, $merge); + if ($return instanceof Paginator) { + $return = $this; + } + + return $return; + } + + /** + * Proxy setting config options to Paginator. + * + * @param string|array $key The key to set, or a complete array of configs. + * @param mixed|null $value The value to set. + * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true. + * @return $this + */ + public function setConfig($key, $value = null, $merge = true) + { + $this->_paginator->setConfig($key, $value, $merge); + + return $this; + } + + /** + * Proxy getting config options to Paginator. + * + * @param string|null $key The key to get or null for the whole config. + * @param mixed $default The return value when the key does not exist. + * @return mixed Config value being read. + */ + public function getConfig($key = null, $default = null) + { + return $this->_paginator->getConfig($key, $default); + } + + /** + * Proxy setting config options to Paginator. + * + * @param string|array $key The key to set, or a complete array of configs. + * @param mixed|null $value The value to set. + * @return $this + */ + public function configShallow($key, $value = null) + { + $this->_paginator->configShallow($key, null); + + return $this; + } + + /** + * Proxy method calls to Paginator. + * + * @param string $method Method name. + * @param array $args Method arguments. + * @return mixed + */ + public function __call($method, $args) + { + return call_user_func_array([$this->_paginator, $method], $args); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/Component/RequestHandlerComponent.php b/app/vendor/cakephp/cakephp/src/Controller/Component/RequestHandlerComponent.php new file mode 100644 index 000000000..f0ad7d279 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/Component/RequestHandlerComponent.php @@ -0,0 +1,763 @@ + true, + 'viewClassMap' => [], + 'inputTypeMap' => [], + 'enableBeforeRedirect' => true + ]; + + /** + * Set the layout to be used when rendering the AuthComponent's ajaxLogin element. + * + * @var string + * @deprecated 3.3.11 This feature property is not supported and will + * be removed in 4.0.0 + */ + public $ajaxLayout; + + /** + * Constructor. Parses the accepted content types accepted by the client using HTTP_ACCEPT + * + * @param \Cake\Controller\ComponentRegistry $registry ComponentRegistry object. + * @param array $config Array of config. + */ + public function __construct(ComponentRegistry $registry, array $config = []) + { + $config += [ + 'viewClassMap' => [ + 'json' => 'Json', + 'xml' => 'Xml', + 'ajax' => 'Ajax' + ], + 'inputTypeMap' => [ + 'json' => ['json_decode', true], + 'xml' => [[$this, 'convertXml']], + ] + ]; + parent::__construct($registry, $config); + } + + /** + * Events supported by this component. + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Controller.startup' => 'startup', + 'Controller.beforeRender' => 'beforeRender', + 'Controller.beforeRedirect' => 'beforeRedirect', + ]; + } + + /** + * @param array $config The config data. + * @return void + * @deprecated 3.4.0 Unused. To be removed in 4.0.0 + */ + public function initialize(array $config) + { + } + + /** + * Set the extension based on the accept headers. + * Compares the accepted types and configured extensions. + * If there is one common type, that is assigned as the ext/content type for the response. + * The type with the highest weight will be set. If the highest weight has more + * than one type matching the extensions, the order in which extensions are specified + * determines which type will be set. + * + * If html is one of the preferred types, no content type will be set, this + * is to avoid issues with browsers that prefer HTML and several other content types. + * + * @param \Cake\Http\ServerRequest $request The request instance. + * @param \Cake\Http\Response $response The response instance. + * @return void + */ + protected function _setExtension($request, $response) + { + $accept = $request->parseAccept(); + if (empty($accept) || current($accept)[0] === 'text/html') { + return; + } + + $accepts = $response->mapType($accept); + $preferredTypes = current($accepts); + if (array_intersect($preferredTypes, ['html', 'xhtml'])) { + return; + } + + $extensions = array_unique( + array_merge(Router::extensions(), array_keys($this->getConfig('viewClassMap'))) + ); + foreach ($accepts as $types) { + $ext = array_intersect($extensions, $types); + if ($ext) { + $this->ext = current($ext); + break; + } + } + } + + /** + * The startup method of the RequestHandler enables several automatic behaviors + * related to the detection of certain properties of the HTTP request, including: + * + * If the XML data is POSTed, the data is parsed into an XML object, which is assigned + * to the $data property of the controller, which can then be saved to a model object. + * + * @param \Cake\Event\Event $event The startup event that was fired. + * @return void + */ + public function startup(Event $event) + { + /** @var \Cake\Controller\Controller $controller */ + $controller = $event->getSubject(); + $request = $controller->getRequest(); + $response = $controller->getResponse(); + + if ($request->getParam('_ext')) { + $this->ext = $request->getParam('_ext'); + } + if (!$this->ext || in_array($this->ext, ['html', 'htm'])) { + $this->_setExtension($request, $response); + } + + $isAjax = $request->is('ajax'); + $controller->setRequest($request->withParam('isAjax', $isAjax)); + + if (!$this->ext && $isAjax) { + $this->ext = 'ajax'; + } + + if ($request->is(['get', 'head', 'options'])) { + return; + } + + if ($request->getParsedBody() !== []) { + return; + } + foreach ($this->getConfig('inputTypeMap') as $type => $handler) { + if (!is_callable($handler[0])) { + throw new RuntimeException(sprintf("Invalid callable for '%s' type.", $type)); + } + if ($this->requestedWith($type)) { + $input = $request->input(...$handler); + $controller->setRequest($request->withParsedBody((array)$input)); + } + } + } + + /** + * Helper method to parse xml input data, due to lack of anonymous functions + * this lives here. + * + * @param string $xml XML string. + * @return array Xml array data + */ + public function convertXml($xml) + { + try { + $xml = Xml::build($xml, ['return' => 'domdocument', 'readFile' => false]); + // We might not get child nodes if there are nested inline entities. + if ($xml->childNodes->length > 0) { + return Xml::toArray($xml); + } + + return []; + } catch (XmlException $e) { + return []; + } + } + + /** + * Handles (fakes) redirects for AJAX requests using requestAction() + * + * @param \Cake\Event\Event $event The Controller.beforeRedirect event. + * @param string|array $url A string or array containing the redirect location + * @param \Cake\Http\Response $response The response object. + * @return \Cake\Http\Response|null The response object if the redirect is caught. + * @deprecated 3.3.5 This functionality will be removed in 4.0.0. You can disable this function + * now by setting the `enableBeforeRedirect` config option to false. + */ + public function beforeRedirect(Event $event, $url, Response $response) + { + if (!$this->getConfig('enableBeforeRedirect')) { + return null; + } + deprecationWarning( + 'RequestHandlerComponent::beforeRedirect() is deprecated. ' . + 'This functionality will be removed in 4.0.0. Set the `enableBeforeRedirect` ' . + 'option to `false` to disable this warning.' + ); + $request = $this->getController()->getRequest(); + if (!$request->is('ajax')) { + return null; + } + if (empty($url)) { + return null; + } + if (is_array($url)) { + $url = Router::url($url + ['_base' => false]); + } + $query = []; + if (strpos($url, '?') !== false) { + list($url, $querystr) = explode('?', $url, 2); + parse_str($querystr, $query); + } + /** @var \Cake\Controller\Controller $controller */ + $controller = $event->getSubject(); + $response->body($controller->requestAction($url, [ + 'return', + 'bare' => false, + 'environment' => [ + 'REQUEST_METHOD' => 'GET' + ], + 'query' => $query, + 'cookies' => $request->getCookieParams() + ])); + + return $response->withStatus(200); + } + + /** + * Checks if the response can be considered different according to the request + * headers, and the caching response headers. If it was not modified, then the + * render process is skipped. And the client will get a blank response with a + * "304 Not Modified" header. + * + * - If Router::extensions() is enabled, the layout and template type are + * switched based on the parsed extension or `Accept` header. For example, + * if `controller/action.xml` is requested, the view path becomes + * `app/View/Controller/xml/action.ctp`. Also if `controller/action` is + * requested with `Accept: application/xml` in the headers the view + * path will become `app/View/Controller/xml/action.ctp`. Layout and template + * types will only switch to mime-types recognized by Cake\Http\Response. + * If you need to declare additional mime-types, you can do so using + * Cake\Http\Response::type() in your controller's beforeFilter() method. + * - If a helper with the same name as the extension exists, it is added to + * the controller. + * - If the extension is of a type that RequestHandler understands, it will + * set that Content-type in the response header. + * + * @param \Cake\Event\Event $event The Controller.beforeRender event. + * @return bool false if the render process should be aborted + */ + public function beforeRender(Event $event) + { + /** @var \Cake\Controller\Controller $controller */ + $controller = $event->getSubject(); + $response = $controller->getResponse(); + $request = $controller->getRequest(); + + $isRecognized = ( + !in_array($this->ext, ['html', 'htm']) && + $response->getMimeType($this->ext) + ); + + if ($this->ext && $isRecognized) { + $this->renderAs($controller, $this->ext); + $response = $controller->response; + } else { + $response = $response->withCharset(Configure::read('App.encoding')); + } + + if ($this->_config['checkHttpCache'] && + $response->checkNotModified($request) + ) { + $controller->setResponse($response); + + return false; + } + $controller->setResponse($response); + } + + /** + * Returns true if the current call accepts an XML response, false otherwise + * + * @return bool True if client accepts an XML response + */ + public function isXml() + { + return $this->prefers('xml'); + } + + /** + * Returns true if the current call accepts an RSS response, false otherwise + * + * @return bool True if client accepts an RSS response + */ + public function isRss() + { + return $this->prefers('rss'); + } + + /** + * Returns true if the current call accepts an Atom response, false otherwise + * + * @return bool True if client accepts an RSS response + */ + public function isAtom() + { + return $this->prefers('atom'); + } + + /** + * Returns true if user agent string matches a mobile web browser, or if the + * client accepts WAP content. + * + * @return bool True if user agent is a mobile web browser + */ + public function isMobile() + { + $request = $this->getController()->getRequest(); + + return $request->is('mobile') || $this->accepts('wap'); + } + + /** + * Returns true if the client accepts WAP content + * + * @return bool + */ + public function isWap() + { + return $this->prefers('wap'); + } + + /** + * Determines which content types the client accepts. Acceptance is based on + * the file extension parsed by the Router (if present), and by the HTTP_ACCEPT + * header. Unlike Cake\Http\ServerRequest::accepts() this method deals entirely with mapped content types. + * + * Usage: + * + * ``` + * $this->RequestHandler->accepts(['xml', 'html', 'json']); + * ``` + * + * Returns true if the client accepts any of the supplied types. + * + * ``` + * $this->RequestHandler->accepts('xml'); + * ``` + * + * Returns true if the client accepts xml. + * + * @param string|array|null $type Can be null (or no parameter), a string type name, or an + * array of types + * @return mixed If null or no parameter is passed, returns an array of content + * types the client accepts. If a string is passed, returns true + * if the client accepts it. If an array is passed, returns true + * if the client accepts one or more elements in the array. + */ + public function accepts($type = null) + { + $controller = $this->getController(); + $request = $controller->getRequest(); + $response = $controller->getResponse(); + $accepted = $request->accepts(); + + if (!$type) { + return $response->mapType($accepted); + } + if (is_array($type)) { + foreach ($type as $t) { + $t = $this->mapAlias($t); + if (in_array($t, $accepted)) { + return true; + } + } + + return false; + } + if (is_string($type)) { + return in_array($this->mapAlias($type), $accepted); + } + + return false; + } + + /** + * Determines the content type of the data the client has sent (i.e. in a POST request) + * + * @param string|array|null $type Can be null (or no parameter), a string type name, or an array of types + * @return mixed If a single type is supplied a boolean will be returned. If no type is provided + * The mapped value of CONTENT_TYPE will be returned. If an array is supplied the first type + * in the request content type will be returned. + */ + public function requestedWith($type = null) + { + $controller = $this->getController(); + $request = $controller->getRequest(); + $response = $controller->getResponse(); + + if (!$request->is('post') && + !$request->is('put') && + !$request->is('patch') && + !$request->is('delete') + ) { + return null; + } + if (is_array($type)) { + foreach ($type as $t) { + if ($this->requestedWith($t)) { + return $t; + } + } + + return false; + } + + list($contentType) = explode(';', $request->contentType()); + if ($type === null) { + return $response->mapType($contentType); + } + if (is_string($type)) { + return ($type === $response->mapType($contentType)); + } + } + + /** + * Determines which content-types the client prefers. If no parameters are given, + * the single content-type that the client most likely prefers is returned. If $type is + * an array, the first item in the array that the client accepts is returned. + * Preference is determined primarily by the file extension parsed by the Router + * if provided, and secondarily by the list of content-types provided in + * HTTP_ACCEPT. + * + * @param string|array|null $type An optional array of 'friendly' content-type names, i.e. + * 'html', 'xml', 'js', etc. + * @return mixed If $type is null or not provided, the first content-type in the + * list, based on preference, is returned. If a single type is provided + * a boolean will be returned if that type is preferred. + * If an array of types are provided then the first preferred type is returned. + * If no type is provided the first preferred type is returned. + */ + public function prefers($type = null) + { + $controller = $this->getController(); + $request = $controller->getRequest(); + $response = $controller->getResponse(); + $acceptRaw = $request->parseAccept(); + + if (empty($acceptRaw)) { + return $type ? $type === $this->ext : $this->ext; + } + $accepts = $response->mapType(array_shift($acceptRaw)); + + if (!$type) { + if (empty($this->ext) && !empty($accepts)) { + return $accepts[0]; + } + + return $this->ext; + } + + $types = (array)$type; + + if (count($types) === 1) { + if ($this->ext) { + return in_array($this->ext, $types); + } + + return in_array($types[0], $accepts); + } + + $intersect = array_values(array_intersect($accepts, $types)); + if (!$intersect) { + return false; + } + + return $intersect[0]; + } + + /** + * Sets either the view class if one exists or the layout and template path of the view. + * The names of these are derived from the $type input parameter. + * + * ### Usage: + * + * Render the response as an 'ajax' response. + * + * ``` + * $this->RequestHandler->renderAs($this, 'ajax'); + * ``` + * + * Render the response as an xml file and force the result as a file download. + * + * ``` + * $this->RequestHandler->renderAs($this, 'xml', ['attachment' => 'myfile.xml']; + * ``` + * + * @param \Cake\Controller\Controller $controller A reference to a controller object + * @param string $type Type of response to send (e.g: 'ajax') + * @param array $options Array of options to use + * @return void + * @see \Cake\Controller\Component\RequestHandlerComponent::respondAs() + */ + public function renderAs(Controller $controller, $type, array $options = []) + { + $defaults = ['charset' => 'UTF-8']; + $viewClassMap = $this->getConfig('viewClassMap'); + + if (Configure::read('App.encoding') !== null) { + $defaults['charset'] = Configure::read('App.encoding'); + } + $options += $defaults; + + $builder = $controller->viewBuilder(); + if (array_key_exists($type, $viewClassMap)) { + $view = $viewClassMap[$type]; + } else { + $view = Inflector::classify($type); + } + + $viewClass = null; + if ($builder->getClassName() === null) { + $viewClass = App::className($view, 'View', 'View'); + } + + if ($viewClass) { + $controller->viewClass = $viewClass; + $builder->setClassName($viewClass); + } else { + if (!$this->_renderType) { + $builder->setTemplatePath($builder->getTemplatePath() . DIRECTORY_SEPARATOR . $type); + } else { + $builder->setTemplatePath(preg_replace( + "/([\/\\\\]{$this->_renderType})$/", + DIRECTORY_SEPARATOR . $type, + $builder->getTemplatePath() + )); + } + + $this->_renderType = $type; + $builder->setLayoutPath($type); + } + + $response = $controller->response; + if ($response->getMimeType($type)) { + $this->respondAs($type, $options); + } + } + + /** + * Sets the response header based on type map index name. This wraps several methods + * available on Cake\Http\Response. It also allows you to use Content-Type aliases. + * + * @param string|array $type Friendly type name, i.e. 'html' or 'xml', or a full content-type, + * like 'application/x-shockwave'. + * @param array $options If $type is a friendly type name that is associated with + * more than one type of content, $index is used to select which content-type to use. + * @return bool Returns false if the friendly type name given in $type does + * not exist in the type map, or if the Content-type header has + * already been set by this method. + */ + public function respondAs($type, array $options = []) + { + $defaults = ['index' => null, 'charset' => null, 'attachment' => false]; + $options += $defaults; + + $cType = $type; + $controller = $this->getController(); + $response = $controller->getResponse(); + $request = $controller->getRequest(); + + if (strpos($type, '/') === false) { + $cType = $response->getMimeType($type); + } + if (is_array($cType)) { + if (isset($cType[$options['index']])) { + $cType = $cType[$options['index']]; + } + + if ($this->prefers($cType)) { + $cType = $this->prefers($cType); + } else { + $cType = $cType[0]; + } + } + + if (!$type) { + return false; + } + if (!$request->getParam('requested')) { + $response = $response->withType($cType); + } + if (!empty($options['charset'])) { + $response = $response->withCharset($options['charset']); + } + if (!empty($options['attachment'])) { + $response = $response->withDownload($options['attachment']); + } + $controller->setResponse($response); + + return true; + } + + /** + * Returns the current response type (Content-type header), or null if not alias exists + * + * @return mixed A string content type alias, or raw content type if no alias map exists, + * otherwise null + */ + public function responseType() + { + $response = $this->getController()->response; + + return $response->mapType($response->getType()); + } + + /** + * Maps a content type alias back to its mime-type(s) + * + * @param string|array $alias String alias to convert back into a content type. Or an array of aliases to map. + * @return string|null|array Null on an undefined alias. String value of the mapped alias type. If an + * alias maps to more than one content type, the first one will be returned. If an array is provided + * for $alias, an array of mapped types will be returned. + */ + public function mapAlias($alias) + { + if (is_array($alias)) { + return array_map([$this, 'mapAlias'], $alias); + } + $response = $this->getController()->response; + $type = $response->getMimeType($alias); + if ($type) { + if (is_array($type)) { + return $type[0]; + } + + return $type; + } + + return null; + } + + /** + * Add a new mapped input type. Mapped input types are automatically + * converted by RequestHandlerComponent during the startup() callback. + * + * @param string $type The type alias being converted, ie. json + * @param array $handler The handler array for the type. The first index should + * be the handling callback, all other arguments should be additional parameters + * for the handler. + * @return void + * @throws \Cake\Core\Exception\Exception + * @deprecated 3.1.0 Use setConfig('addInputType', ...) instead. + */ + public function addInputType($type, $handler) + { + deprecationWarning( + 'RequestHandlerComponent::addInputType() is deprecated. Use setConfig("inputTypeMap", ...) instead.' + ); + if (!is_array($handler) || !isset($handler[0]) || !is_callable($handler[0])) { + throw new Exception('You must give a handler callback.'); + } + $this->setConfig('inputTypeMap.' . $type, $handler); + } + + /** + * Getter/setter for viewClassMap + * + * @param array|string|null $type The type string or array with format `['type' => 'viewClass']` to map one or more + * @param array|null $viewClass The viewClass to be used for the type without `View` appended + * @return array|string Returns viewClass when only string $type is set, else array with viewClassMap + * @deprecated 3.1.0 Use setConfig('viewClassMap', ...) instead. + */ + public function viewClassMap($type = null, $viewClass = null) + { + deprecationWarning( + 'RequestHandlerComponent::viewClassMap() is deprecated. Use setConfig("viewClassMap", ...) instead.' + ); + if (!$viewClass && is_string($type)) { + return $this->getConfig('viewClassMap.' . $type); + } + if (is_string($type)) { + $this->setConfig('viewClassMap.' . $type, $viewClass); + } elseif (is_array($type)) { + $this->setConfig('viewClassMap', $type, true); + } + + return $this->getConfig('viewClassMap'); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/Component/SecurityComponent.php b/app/vendor/cakephp/cakephp/src/Controller/Component/SecurityComponent.php new file mode 100644 index 000000000..e3ca37057 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/Component/SecurityComponent.php @@ -0,0 +1,668 @@ + null, + 'requireSecure' => [], + 'requireAuth' => [], + 'allowedControllers' => [], + 'allowedActions' => [], + 'unlockedFields' => [], + 'unlockedActions' => [], + 'validatePost' => true + ]; + + /** + * Holds the current action of the controller + * + * @var string + */ + protected $_action; + + /** + * The Session object + * + * @var \Cake\Http\Session + */ + public $session; + + /** + * Component startup. All security checking happens here. + * + * @param \Cake\Event\Event $event An Event instance + * @return mixed + */ + public function startup(Event $event) + { + /** @var \Cake\Controller\Controller $controller */ + $controller = $event->getSubject(); + $request = $controller->request; + $this->session = $request->getSession(); + $this->_action = $request->getParam('action'); + $hasData = ($request->getData() || $request->is(['put', 'post', 'delete', 'patch'])); + try { + $this->_secureRequired($controller); + $this->_authRequired($controller); + + $isNotRequestAction = !$request->getParam('requested'); + + if ($this->_action === $this->_config['blackHoleCallback']) { + throw new AuthSecurityException(sprintf('Action %s is defined as the blackhole callback.', $this->_action)); + } + + if (!in_array($this->_action, (array)$this->_config['unlockedActions']) && + $hasData && + $isNotRequestAction && + $this->_config['validatePost'] + ) { + $this->_validatePost($controller); + } + } catch (SecurityException $se) { + $this->blackHole($controller, $se->getType(), $se); + } + + $request = $this->generateToken($request); + if ($hasData && is_array($controller->getRequest()->getData())) { + $request = $request->withoutData('_Token'); + } + $controller->setRequest($request); + } + + /** + * Events supported by this component. + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Controller.startup' => 'startup', + ]; + } + + /** + * Sets the actions that require a request that is SSL-secured, or empty for all actions + * + * @param string|array|null $actions Actions list + * @return void + */ + public function requireSecure($actions = null) + { + $this->_requireMethod('Secure', (array)$actions); + } + + /** + * Sets the actions that require whitelisted form submissions. + * + * Adding actions with this method will enforce the restrictions + * set in SecurityComponent::$allowedControllers and + * SecurityComponent::$allowedActions. + * + * @param string|array $actions Actions list + * @return void + * @deprecated 3.2.2 This feature is confusing and not useful. + */ + public function requireAuth($actions) + { + deprecationWarning('SecurityComponent::requireAuth() will be removed in 4.0.0.'); + $this->_requireMethod('Auth', (array)$actions); + } + + /** + * Black-hole an invalid request with a 400 error or custom callback. If SecurityComponent::$blackHoleCallback + * is specified, it will use this callback by executing the method indicated in $error + * + * @param \Cake\Controller\Controller $controller Instantiating controller + * @param string $error Error method + * @param \Cake\Controller\Exception\SecurityException|null $exception Additional debug info describing the cause + * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise + * @see \Cake\Controller\Component\SecurityComponent::$blackHoleCallback + * @link https://book.cakephp.org/3.0/en/controllers/components/security.html#handling-blackhole-callbacks + * @throws \Cake\Http\Exception\BadRequestException + */ + public function blackHole(Controller $controller, $error = '', SecurityException $exception = null) + { + if (!$this->_config['blackHoleCallback']) { + $this->_throwException($exception); + } + + return $this->_callback($controller, $this->_config['blackHoleCallback'], [$error, $exception]); + } + + /** + * Check debug status and throw an Exception based on the existing one + * + * @param \Cake\Controller\Exception\SecurityException|null $exception Additional debug info describing the cause + * @throws \Cake\Http\Exception\BadRequestException + * @return void + */ + protected function _throwException($exception = null) + { + if ($exception !== null) { + if (!Configure::read('debug') && $exception instanceof SecurityException) { + $exception->setReason($exception->getMessage()); + $exception->setMessage(self::DEFAULT_EXCEPTION_MESSAGE); + } + throw $exception; + } + throw new BadRequestException(self::DEFAULT_EXCEPTION_MESSAGE); + } + + /** + * Sets the actions that require a $method HTTP request, or empty for all actions + * + * @param string $method The HTTP method to assign controller actions to + * @param array $actions Controller actions to set the required HTTP method to. + * @return void + */ + protected function _requireMethod($method, $actions = []) + { + if (isset($actions[0]) && is_array($actions[0])) { + $actions = $actions[0]; + } + $this->setConfig('require' . $method, empty($actions) ? ['*'] : $actions); + } + + /** + * Check if access requires secure connection + * + * @param \Cake\Controller\Controller $controller Instantiating controller + * @return bool true if secure connection required + */ + protected function _secureRequired(Controller $controller) + { + if (is_array($this->_config['requireSecure']) && + !empty($this->_config['requireSecure']) + ) { + $requireSecure = $this->_config['requireSecure']; + + if (in_array($this->_action, $requireSecure) || $requireSecure === ['*']) { + if (!$this->getController()->getRequest()->is('ssl')) { + throw new SecurityException( + 'Request is not SSL and the action is required to be secure' + ); + } + } + } + + return true; + } + + /** + * Check if authentication is required + * + * @param \Cake\Controller\Controller $controller Instantiating controller + * @return bool true if authentication required + * @deprecated 3.2.2 This feature is confusing and not useful. + */ + protected function _authRequired(Controller $controller) + { + $request = $controller->getRequest(); + if (is_array($this->_config['requireAuth']) && + !empty($this->_config['requireAuth']) && + $request->getData() + ) { + deprecationWarning('SecurityComponent::requireAuth() will be removed in 4.0.0.'); + $requireAuth = $this->_config['requireAuth']; + + if (in_array($request->getParam('action'), $requireAuth) || $requireAuth == ['*']) { + if ($request->getData('_Token') === null) { + throw new AuthSecurityException('\'_Token\' was not found in request data.'); + } + + if ($this->session->check('_Token')) { + $tData = $this->session->read('_Token'); + + if (!empty($tData['allowedControllers']) && + !in_array($request->getParam('controller'), $tData['allowedControllers'])) { + throw new AuthSecurityException( + sprintf( + 'Controller \'%s\' was not found in allowed controllers: \'%s\'.', + $request->getParam('controller'), + implode(', ', (array)$tData['allowedControllers']) + ) + ); + } + if (!empty($tData['allowedActions']) && + !in_array($request->getParam('action'), $tData['allowedActions']) + ) { + throw new AuthSecurityException( + sprintf( + 'Action \'%s::%s\' was not found in allowed actions: \'%s\'.', + $request->getParam('controller'), + $request->getParam('action'), + implode(', ', (array)$tData['allowedActions']) + ) + ); + } + } else { + throw new AuthSecurityException('\'_Token\' was not found in session.'); + } + } + } + + return true; + } + + /** + * Validate submitted form + * + * @param \Cake\Controller\Controller $controller Instantiating controller + * @throws \Cake\Controller\Exception\AuthSecurityException + * @return bool true if submitted form is valid + */ + protected function _validatePost(Controller $controller) + { + $token = $this->_validToken($controller); + $hashParts = $this->_hashParts($controller); + $check = hash_hmac('sha1', implode('', $hashParts), Security::getSalt()); + + if (hash_equals($check, $token)) { + return true; + } + + $msg = self::DEFAULT_EXCEPTION_MESSAGE; + if (Configure::read('debug')) { + $msg = $this->_debugPostTokenNotMatching($controller, $hashParts); + } + + throw new AuthSecurityException($msg); + } + + /** + * Check if token is valid + * + * @param \Cake\Controller\Controller $controller Instantiating controller + * @throws \Cake\Controller\Exception\SecurityException + * @return string fields token + */ + protected function _validToken(Controller $controller) + { + $check = $controller->getRequest()->getData(); + + $message = '\'%s\' was not found in request data.'; + if (!isset($check['_Token'])) { + throw new AuthSecurityException(sprintf($message, '_Token')); + } + if (!isset($check['_Token']['fields'])) { + throw new AuthSecurityException(sprintf($message, '_Token.fields')); + } + if (!isset($check['_Token']['unlocked'])) { + throw new AuthSecurityException(sprintf($message, '_Token.unlocked')); + } + if (Configure::read('debug') && !isset($check['_Token']['debug'])) { + throw new SecurityException(sprintf($message, '_Token.debug')); + } + if (!Configure::read('debug') && isset($check['_Token']['debug'])) { + throw new SecurityException('Unexpected \'_Token.debug\' found in request data'); + } + + $token = urldecode($check['_Token']['fields']); + if (strpos($token, ':')) { + list($token, ) = explode(':', $token, 2); + } + + return $token; + } + + /** + * Return hash parts for the Token generation + * + * @param \Cake\Controller\Controller $controller Instantiating controller + * @return array + */ + protected function _hashParts(Controller $controller) + { + $request = $controller->getRequest(); + + // Start the session to ensure we get the correct session id. + $session = $request->getSession(); + $session->start(); + + $data = $request->getData(); + $fieldList = $this->_fieldsList($data); + $unlocked = $this->_sortedUnlocked($data); + + return [ + Router::url($request->getRequestTarget()), + serialize($fieldList), + $unlocked, + $session->id() + ]; + } + + /** + * Return the fields list for the hash calculation + * + * @param array $check Data array + * @return array + */ + protected function _fieldsList(array $check) + { + $locked = ''; + $token = urldecode($check['_Token']['fields']); + $unlocked = $this->_unlocked($check); + + if (strpos($token, ':')) { + list($token, $locked) = explode(':', $token, 2); + } + unset($check['_Token'], $check['_csrfToken']); + + $locked = explode('|', $locked); + $unlocked = explode('|', $unlocked); + + $fields = Hash::flatten($check); + $fieldList = array_keys($fields); + $multi = $lockedFields = []; + $isUnlocked = false; + + foreach ($fieldList as $i => $key) { + if (preg_match('/(\.\d+){1,10}$/', $key)) { + $multi[$i] = preg_replace('/(\.\d+){1,10}$/', '', $key); + unset($fieldList[$i]); + } else { + $fieldList[$i] = (string)$key; + } + } + if (!empty($multi)) { + $fieldList += array_unique($multi); + } + + $unlockedFields = array_unique( + array_merge((array)$this->getConfig('disabledFields'), (array)$this->_config['unlockedFields'], $unlocked) + ); + + foreach ($fieldList as $i => $key) { + $isLocked = (is_array($locked) && in_array($key, $locked)); + + if (!empty($unlockedFields)) { + foreach ($unlockedFields as $off) { + $off = explode('.', $off); + $field = array_values(array_intersect(explode('.', $key), $off)); + $isUnlocked = ($field === $off); + if ($isUnlocked) { + break; + } + } + } + + if ($isUnlocked || $isLocked) { + unset($fieldList[$i]); + if ($isLocked) { + $lockedFields[$key] = $fields[$key]; + } + } + } + sort($fieldList, SORT_STRING); + ksort($lockedFields, SORT_STRING); + $fieldList += $lockedFields; + + return $fieldList; + } + + /** + * Get the unlocked string + * + * @param array $data Data array + * @return string + */ + protected function _unlocked(array $data) + { + return urldecode($data['_Token']['unlocked']); + } + + /** + * Get the sorted unlocked string + * + * @param array $data Data array + * @return string + */ + protected function _sortedUnlocked($data) + { + $unlocked = $this->_unlocked($data); + $unlocked = explode('|', $unlocked); + sort($unlocked, SORT_STRING); + + return implode('|', $unlocked); + } + + /** + * Create a message for humans to understand why Security token is not matching + * + * @param \Cake\Controller\Controller $controller Instantiating controller + * @param array $hashParts Elements used to generate the Token hash + * @return string Message explaining why the tokens are not matching + */ + protected function _debugPostTokenNotMatching(Controller $controller, $hashParts) + { + $messages = []; + $expectedParts = json_decode(urldecode($controller->getRequest()->getData('_Token.debug')), true); + if (!is_array($expectedParts) || count($expectedParts) !== 3) { + return 'Invalid security debug token.'; + } + $expectedUrl = Hash::get($expectedParts, 0); + $url = Hash::get($hashParts, 0); + if ($expectedUrl !== $url) { + $messages[] = sprintf('URL mismatch in POST data (expected \'%s\' but found \'%s\')', $expectedUrl, $url); + } + $expectedFields = Hash::get($expectedParts, 1); + $dataFields = Hash::get($hashParts, 1); + if ($dataFields) { + $dataFields = unserialize($dataFields); + } + $fieldsMessages = $this->_debugCheckFields( + $dataFields, + $expectedFields, + 'Unexpected field \'%s\' in POST data', + 'Tampered field \'%s\' in POST data (expected value \'%s\' but found \'%s\')', + 'Missing field \'%s\' in POST data' + ); + $expectedUnlockedFields = Hash::get($expectedParts, 2); + $dataUnlockedFields = Hash::get($hashParts, 2) ?: null; + if ($dataUnlockedFields) { + $dataUnlockedFields = explode('|', $dataUnlockedFields); + } + $unlockFieldsMessages = $this->_debugCheckFields( + (array)$dataUnlockedFields, + $expectedUnlockedFields, + 'Unexpected unlocked field \'%s\' in POST data', + null, + 'Missing unlocked field: \'%s\'' + ); + + $messages = array_merge($messages, $fieldsMessages, $unlockFieldsMessages); + + return implode(', ', $messages); + } + + /** + * Iterates data array to check against expected + * + * @param array $dataFields Fields array, containing the POST data fields + * @param array $expectedFields Fields array, containing the expected fields we should have in POST + * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected) + * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected) + * @param string $missingMessage Message string if missing field + * @return array Messages + */ + protected function _debugCheckFields($dataFields, $expectedFields = [], $intKeyMessage = '', $stringKeyMessage = '', $missingMessage = '') + { + $messages = $this->_matchExistingFields($dataFields, $expectedFields, $intKeyMessage, $stringKeyMessage); + $expectedFieldsMessage = $this->_debugExpectedFields($expectedFields, $missingMessage); + if ($expectedFieldsMessage !== null) { + $messages[] = $expectedFieldsMessage; + } + + return $messages; + } + + /** + * Manually add form tampering prevention token information into the provided + * request object. + * + * @param \Cake\Http\ServerRequest $request The request object to add into. + * @return \Cake\Http\ServerRequest The modified request. + */ + public function generateToken(ServerRequest $request) + { + if ($request->is('requested')) { + if ($this->session->check('_Token')) { + $request = $request->withParam('_Token', $this->session->read('_Token')); + } + + return $request; + } + $token = [ + 'allowedControllers' => $this->_config['allowedControllers'], + 'allowedActions' => $this->_config['allowedActions'], + 'unlockedFields' => $this->_config['unlockedFields'], + ]; + + $this->session->write('_Token', $token); + + return $request->withParam('_Token', [ + 'unlockedFields' => $token['unlockedFields'] + ]); + } + + /** + * Calls a controller callback method + * + * @param \Cake\Controller\Controller $controller Instantiating controller + * @param string $method Method to execute + * @param array $params Parameters to send to method + * @return mixed Controller callback method's response + * @throws \Cake\Http\Exception\BadRequestException When a the blackholeCallback is not callable. + */ + protected function _callback(Controller $controller, $method, $params = []) + { + if (!is_callable([$controller, $method])) { + throw new BadRequestException('The request has been black-holed'); + } + + return call_user_func_array([&$controller, $method], empty($params) ? null : $params); + } + + /** + * Generate array of messages for the existing fields in POST data, matching dataFields in $expectedFields + * will be unset + * + * @param array $dataFields Fields array, containing the POST data fields + * @param array $expectedFields Fields array, containing the expected fields we should have in POST + * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected) + * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected) + * @return array Error messages + */ + protected function _matchExistingFields($dataFields, &$expectedFields, $intKeyMessage, $stringKeyMessage) + { + $messages = []; + foreach ((array)$dataFields as $key => $value) { + if (is_int($key)) { + $foundKey = array_search($value, (array)$expectedFields); + if ($foundKey === false) { + $messages[] = sprintf($intKeyMessage, $value); + } else { + unset($expectedFields[$foundKey]); + } + } elseif (is_string($key)) { + if (isset($expectedFields[$key]) && $value !== $expectedFields[$key]) { + $messages[] = sprintf($stringKeyMessage, $key, $expectedFields[$key], $value); + } + unset($expectedFields[$key]); + } + } + + return $messages; + } + + /** + * Generate debug message for the expected fields + * + * @param array $expectedFields Expected fields + * @param string $missingMessage Message template + * @return string|null Error message about expected fields + */ + protected function _debugExpectedFields($expectedFields = [], $missingMessage = '') + { + if (count($expectedFields) === 0) { + return null; + } + + $expectedFieldNames = []; + foreach ((array)$expectedFields as $key => $expectedField) { + if (is_int($key)) { + $expectedFieldNames[] = $expectedField; + } else { + $expectedFieldNames[] = $key; + } + } + + return sprintf($missingMessage, implode(', ', $expectedFieldNames)); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/ComponentRegistry.php b/app/vendor/cakephp/cakephp/src/Controller/ComponentRegistry.php new file mode 100644 index 000000000..f395c8c22 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/ComponentRegistry.php @@ -0,0 +1,127 @@ +setController($controller); + } + } + + /** + * Get the controller associated with the collection. + * + * @return \Cake\Controller\Controller Controller instance + */ + public function getController() + { + return $this->_Controller; + } + + /** + * Set the controller associated with the collection. + * + * @param \Cake\Controller\Controller $controller Controller instance. + * @return void + */ + public function setController(Controller $controller) + { + $this->_Controller = $controller; + $this->setEventManager($controller->getEventManager()); + } + + /** + * Resolve a component classname. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * + * @param string $class Partial classname to resolve. + * @return string|false Either the correct classname or false. + */ + protected function _resolveClassName($class) + { + return App::className($class, 'Controller/Component', 'Component'); + } + + /** + * Throws an exception when a component is missing. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * and Cake\Core\ObjectRegistry::unload() + * + * @param string $class The classname that is missing. + * @param string $plugin The plugin the component is missing in. + * @return void + * @throws \Cake\Controller\Exception\MissingComponentException + */ + protected function _throwMissingClassError($class, $plugin) + { + throw new MissingComponentException([ + 'class' => $class . 'Component', + 'plugin' => $plugin + ]); + } + + /** + * Create the component instance. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * Enabled components will be registered with the event manager. + * + * @param string $class The classname to create. + * @param string $alias The alias of the component. + * @param array $config An array of config to use for the component. + * @return \Cake\Controller\Component The constructed component class. + */ + protected function _create($class, $alias, $config) + { + $instance = new $class($this, $config); + $enable = isset($config['enabled']) ? $config['enabled'] : true; + if ($enable) { + $this->getEventManager()->on($instance); + } + + return $instance; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/Controller.php b/app/vendor/cakephp/cakephp/src/Controller/Controller.php new file mode 100644 index 000000000..7d886b8da --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/Controller.php @@ -0,0 +1,959 @@ +request`. The request object + * contains all the POST, GET and FILES that were part of the request. + * + * After performing the required action, controllers are responsible for + * creating a response. This usually takes the form of a generated `View`, or + * possibly a redirection to another URL. In either case `$this->response` + * allows you to manipulate all aspects of the response. + * + * Controllers are created by `Dispatcher` based on request parameters and + * routing. By default controllers and actions use conventional names. + * For example `/posts/index` maps to `PostsController::index()`. You can re-map + * URLs using Router::connect() or RouterBuilder::connect(). + * + * ### Life cycle callbacks + * + * CakePHP fires a number of life cycle callbacks during each request. + * By implementing a method you can receive the related events. The available + * callbacks are: + * + * - `beforeFilter(Event $event)` + * Called before each action. This is a good place to do general logic that + * applies to all actions. + * - `beforeRender(Event $event)` + * Called before the view is rendered. + * - `beforeRedirect(Event $event, $url, Response $response)` + * Called before a redirect is done. + * - `afterFilter(Event $event)` + * Called after each action is complete and after the view is rendered. + * + * @property \Cake\Controller\Component\AuthComponent $Auth + * @property \Cake\Controller\Component\CookieComponent $Cookie + * @property \Cake\Controller\Component\CsrfComponent $Csrf + * @property \Cake\Controller\Component\FlashComponent $Flash + * @property \Cake\Controller\Component\PaginatorComponent $Paginator + * @property \Cake\Controller\Component\RequestHandlerComponent $RequestHandler + * @property \Cake\Controller\Component\SecurityComponent $Security + * @method bool isAuthorized($user) + * @link https://book.cakephp.org/3.0/en/controllers.html + */ +class Controller implements EventListenerInterface, EventDispatcherInterface +{ + + use EventDispatcherTrait; + use LocatorAwareTrait; + use LogTrait; + use MergeVariablesTrait; + use ModelAwareTrait; + use RequestActionTrait; + use ViewVarsTrait; + + /** + * The name of this controller. Controller names are plural, named after the model they manipulate. + * + * Set automatically using conventions in Controller::__construct(). + * + * @var string + */ + protected $name; + + /** + * An array containing the names of helpers this controller uses. The array elements should + * not contain the "Helper" part of the class name. + * + * Example: + * ``` + * public $helpers = ['Form', 'Html', 'Time']; + * ``` + * + * @var array + * @link https://book.cakephp.org/3.0/en/controllers.html#configuring-helpers-to-load + * + * @deprecated 3.0.0 You should configure helpers in your AppView::initialize() method. + */ + public $helpers = []; + + /** + * An instance of a \Cake\Http\ServerRequest object that contains information about the current request. + * This object contains all the information about a request and several methods for reading + * additional information about the request. + * + * Deprecated 3.6.0: The property will become protected in 4.0.0. Use getRequest()/setRequest instead. + * + * @var \Cake\Http\ServerRequest + * @link https://book.cakephp.org/3.0/en/controllers/request-response.html#request + */ + public $request; + + /** + * An instance of a Response object that contains information about the impending response + * + * Deprecated 3.6.0: The property will become protected in 4.0.0. Use getResponse()/setResponse instead. + + * @var \Cake\Http\Response + * @link https://book.cakephp.org/3.0/en/controllers/request-response.html#response + */ + public $response; + + /** + * The class name to use for creating the response object. + * + * @var string + */ + protected $_responseClass = 'Cake\Http\Response'; + + /** + * Settings for pagination. + * + * Used to pre-configure pagination preferences for the various + * tables your controller will be paginating. + * + * @var array + * @see \Cake\Controller\Component\PaginatorComponent + */ + public $paginate = []; + + /** + * Set to true to automatically render the view + * after action logic. + * + * @var bool + */ + protected $autoRender = true; + + /** + * Instance of ComponentRegistry used to create Components + * + * @var \Cake\Controller\ComponentRegistry + */ + protected $_components; + + /** + * Array containing the names of components this controller uses. Component names + * should not contain the "Component" portion of the class name. + * + * Example: + * ``` + * public $components = ['RequestHandler', 'Acl']; + * ``` + * + * @var array + * @link https://book.cakephp.org/3.0/en/controllers/components.html + * + * @deprecated 3.0.0 You should configure components in your Controller::initialize() method. + */ + public $components = []; + + /** + * Instance of the View created during rendering. Won't be set until after + * Controller::render() is called. + * + * @var \Cake\View\View + * @deprecated 3.1.0 Use viewBuilder() instead. + */ + public $View; + + /** + * These Controller properties will be passed from the Controller to the View as options. + * + * @var array + * @see \Cake\View\View + */ + protected $_validViewOptions = [ + 'passedArgs' + ]; + + /** + * Automatically set to the name of a plugin. + * + * @var string|null + */ + protected $plugin; + + /** + * Holds all passed params. + * + * @var array + * @deprecated 3.1.0 Use `$this->request->getParam('pass')` instead. + */ + public $passedArgs = []; + + /** + * Constructor. + * + * Sets a number of properties based on conventions if they are empty. To override the + * conventions CakePHP uses you can define properties in your class declaration. + * + * @param \Cake\Http\ServerRequest|null $request Request object for this controller. Can be null for testing, + * but expect that features that use the request parameters will not work. + * @param \Cake\Http\Response|null $response Response object for this controller. + * @param string|null $name Override the name useful in testing when using mocks. + * @param \Cake\Event\EventManager|null $eventManager The event manager. Defaults to a new instance. + * @param \Cake\Controller\ComponentRegistry|null $components The component registry. Defaults to a new instance. + */ + public function __construct(ServerRequest $request = null, Response $response = null, $name = null, $eventManager = null, $components = null) + { + if ($name !== null) { + $this->name = $name; + } + + if ($this->name === null && $request && $request->getParam('controller')) { + $this->name = $request->getParam('controller'); + } + + if ($this->name === null) { + list(, $name) = namespaceSplit(get_class($this)); + $this->name = substr($name, 0, -10); + } + + $this->setRequest($request ?: new ServerRequest()); + $this->response = $response ?: new Response(); + + if ($eventManager !== null) { + $this->setEventManager($eventManager); + } + + $this->modelFactory('Table', [$this->getTableLocator(), 'get']); + $plugin = $this->request->getParam('plugin'); + $modelClass = ($plugin ? $plugin . '.' : '') . $this->name; + $this->_setModelClass($modelClass); + + if ($components !== null) { + $this->components($components); + } + + $this->initialize(); + + $this->_mergeControllerVars(); + $this->_loadComponents(); + $this->getEventManager()->on($this); + } + + /** + * Initialization hook method. + * + * Implement this method to avoid having to overwrite + * the constructor and call parent. + * + * @return void + */ + public function initialize() + { + } + + /** + * Get the component registry for this controller. + * + * If called with the first parameter, it will be set as the controller $this->_components property + * + * @param \Cake\Controller\ComponentRegistry|null $components Component registry. + * + * @return \Cake\Controller\ComponentRegistry + */ + public function components($components = null) + { + if ($components === null && $this->_components === null) { + $this->_components = new ComponentRegistry($this); + } + if ($components !== null) { + $components->setController($this); + $this->_components = $components; + } + + return $this->_components; + } + + /** + * Add a component to the controller's registry. + * + * This method will also set the component to a property. + * For example: + * + * ``` + * $this->loadComponent('Acl.Acl'); + * ``` + * + * Will result in a `Toolbar` property being set. + * + * @param string $name The name of the component to load. + * @param array $config The config for the component. + * @return \Cake\Controller\Component + * @throws \Exception + */ + public function loadComponent($name, array $config = []) + { + list(, $prop) = pluginSplit($name); + + return $this->{$prop} = $this->components()->load($name, $config); + } + + /** + * Magic accessor for model autoloading. + * + * @param string $name Property name + * @return bool|object The model instance or false + */ + public function __get($name) + { + $deprecated = [ + 'name' => 'getName', + 'plugin' => 'getPlugin', + 'autoRender' => 'isAutoRenderEnabled', + ]; + if (isset($deprecated[$name])) { + $method = $deprecated[$name]; + deprecationWarning(sprintf('Controller::$%s is deprecated. Use $this->%s() instead.', $name, $method)); + + return $this->{$method}(); + } + + $deprecated = [ + 'layout' => 'getLayout', + 'view' => 'getTemplate', + 'theme' => 'getTheme', + 'autoLayout' => 'isAutoLayoutEnabled', + 'viewPath' => 'getTemplatePath', + 'layoutPath' => 'getLayoutPath', + ]; + if (isset($deprecated[$name])) { + $method = $deprecated[$name]; + deprecationWarning(sprintf('Controller::$%s is deprecated. Use $this->viewBuilder()->%s() instead.', $name, $method)); + + return $this->viewBuilder()->{$method}(); + } + + list($plugin, $class) = pluginSplit($this->modelClass, true); + if ($class !== $name) { + return false; + } + + return $this->loadModel($plugin . $class); + } + + /** + * Magic setter for removed properties. + * + * @param string $name Property name. + * @param mixed $value Value to set. + * @return void + */ + public function __set($name, $value) + { + $deprecated = [ + 'name' => 'setName', + 'plugin' => 'setPlugin' + ]; + if (isset($deprecated[$name])) { + $method = $deprecated[$name]; + deprecationWarning(sprintf('Controller::$%s is deprecated. Use $this->%s() instead.', $name, $method)); + $this->{$method}($value); + + return; + } + if ($name === 'autoRender') { + $value ? $this->enableAutoRender() : $this->disableAutoRender(); + deprecationWarning(sprintf('Controller::$%s is deprecated. Use $this->enableAutoRender/disableAutoRender() instead.', $name)); + + return; + } + $deprecated = [ + 'layout' => 'setLayout', + 'view' => 'setTemplate', + 'theme' => 'setTheme', + 'autoLayout' => 'enableAutoLayout', + 'viewPath' => 'setTemplatePath', + 'layoutPath' => 'setLayoutPath', + ]; + if (isset($deprecated[$name])) { + $method = $deprecated[$name]; + deprecationWarning(sprintf('Controller::$%s is deprecated. Use $this->viewBuilder()->%s() instead.', $name, $method)); + + $this->viewBuilder()->{$method}($value); + + return; + } + + $this->{$name} = $value; + } + + /** + * Returns the controller name. + * + * @return string + * @since 3.6.0 + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the controller name. + * + * @param string $name Controller name. + * @return $this + * @since 3.6.0 + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Returns the plugin name. + * + * @return string|null + * @since 3.6.0 + */ + public function getPlugin() + { + return $this->plugin; + } + + /** + * Sets the plugin name. + * + * @param string $name Plugin name. + * @return $this + * @since 3.6.0 + */ + public function setPlugin($name) + { + $this->plugin = $name; + + return $this; + } + + /** + * Returns true if an action should be rendered automatically. + * + * @return bool + * @since 3.6.0 + */ + public function isAutoRenderEnabled() + { + return $this->autoRender; + } + + /** + * Enable automatic action rendering. + * + * @return $this + * @since 3.6.0 + */ + public function enableAutoRender() + { + $this->autoRender = true; + + return $this; + } + + /** + * Disbale automatic action rendering. + * + * @return $this + * @since 3.6.0 + */ + public function disableAutoRender() + { + $this->autoRender = false; + + return $this; + } + + /** + * Gets the request instance. + * + * @return \Cake\Http\ServerRequest + * @since 3.6.0 + */ + public function getRequest() + { + return $this->request; + } + + /** + * Sets the request objects and configures a number of controller properties + * based on the contents of the request. Controller acts as a proxy for certain View variables + * which must also be updated here. The properties that get set are: + * + * - $this->request - To the $request parameter + * - $this->passedArgs - Same as $request->params['pass] + * + * @param \Cake\Http\ServerRequest $request Request instance. + * @return $this + */ + public function setRequest(ServerRequest $request) + { + $this->request = $request; + $this->plugin = $request->getParam('plugin') ?: null; + + if ($request->getParam('pass')) { + $this->passedArgs = $request->getParam('pass'); + } + + return $this; + } + + /** + * Gets the response instance. + * + * @return \Cake\Http\Response + * @since 3.6.0 + */ + public function getResponse() + { + return $this->response; + } + + /** + * Sets the response instance. + * + * @param \Cake\Http\Response $response Response instance. + * @return $this + * @since 3.6.0 + */ + public function setResponse(Response $response) + { + $this->response = $response; + + return $this; + } + + /** + * Dispatches the controller action. Checks that the action + * exists and isn't private. + * + * @return mixed The resulting response. + * @throws \ReflectionException + */ + public function invokeAction() + { + $request = $this->request; + if (!$request) { + throw new LogicException('No Request object configured. Cannot invoke action'); + } + if (!$this->isAction($request->getParam('action'))) { + throw new MissingActionException([ + 'controller' => $this->name . 'Controller', + 'action' => $request->getParam('action'), + 'prefix' => $request->getParam('prefix') ?: '', + 'plugin' => $request->getParam('plugin'), + ]); + } + /* @var callable $callable */ + $callable = [$this, $request->getParam('action')]; + + return $callable(...array_values($request->getParam('pass'))); + } + + /** + * Merge components, helpers vars from + * parent classes. + * + * @return void + */ + protected function _mergeControllerVars() + { + $this->_mergeVars( + ['components', 'helpers'], + ['associative' => ['components', 'helpers']] + ); + } + + /** + * Returns a list of all events that will fire in the controller during its lifecycle. + * You can override this function to add your own listener callbacks + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Controller.initialize' => 'beforeFilter', + 'Controller.beforeRender' => 'beforeRender', + 'Controller.beforeRedirect' => 'beforeRedirect', + 'Controller.shutdown' => 'afterFilter', + ]; + } + + /** + * Loads the defined components using the Component factory. + * + * @return void + */ + protected function _loadComponents() + { + if (empty($this->components)) { + return; + } + $registry = $this->components(); + $components = $registry->normalizeArray($this->components); + foreach ($components as $properties) { + $this->loadComponent($properties['class'], $properties['config']); + } + } + + /** + * Perform the startup process for this controller. + * Fire the Components and Controller callbacks in the correct order. + * + * - Initializes components, which fires their `initialize` callback + * - Calls the controller `beforeFilter`. + * - triggers Component `startup` methods. + * + * @return \Cake\Http\Response|null + */ + public function startupProcess() + { + $event = $this->dispatchEvent('Controller.initialize'); + if ($event->getResult() instanceof Response) { + return $event->getResult(); + } + $event = $this->dispatchEvent('Controller.startup'); + if ($event->getResult() instanceof Response) { + return $event->getResult(); + } + + return null; + } + + /** + * Perform the various shutdown processes for this controller. + * Fire the Components and Controller callbacks in the correct order. + * + * - triggers the component `shutdown` callback. + * - calls the Controller's `afterFilter` method. + * + * @return \Cake\Http\Response|null + */ + public function shutdownProcess() + { + $event = $this->dispatchEvent('Controller.shutdown'); + if ($event->getResult() instanceof Response) { + return $event->getResult(); + } + + return null; + } + + /** + * Redirects to given $url, after turning off $this->autoRender. + * + * @param string|array $url A string or array-based URL pointing to another location within the app, + * or an absolute URL + * @param int $status HTTP status code (eg: 301) + * @return \Cake\Http\Response|null + * @link https://book.cakephp.org/3.0/en/controllers.html#Controller::redirect + */ + public function redirect($url, $status = 302) + { + $this->autoRender = false; + + if ($status) { + $this->response = $this->response->withStatus($status); + } + + $event = $this->dispatchEvent('Controller.beforeRedirect', [$url, $this->response]); + if ($event->getResult() instanceof Response) { + return $this->response = $event->getResult(); + } + if ($event->isStopped()) { + return null; + } + $response = $this->response; + + if (!$response->getHeaderLine('Location')) { + $response = $response->withLocation(Router::url($url, true)); + } + + return $this->response = $response; + } + + /** + * Internally redirects one action to another. Does not perform another HTTP request unlike Controller::redirect() + * + * Examples: + * + * ``` + * setAction('another_action'); + * setAction('action_with_parameters', $parameter1); + * ``` + * + * @param string $action The new action to be 'redirected' to. + * Any other parameters passed to this method will be passed as parameters to the new action. + * @param array ...$args Arguments passed to the action + * @return mixed Returns the return value of the called action + */ + public function setAction($action, ...$args) + { + $this->setRequest($this->request->withParam('action', $action)); + + return $this->$action(...$args); + } + + /** + * Instantiates the correct view class, hands it its data, and uses it to render the view output. + * + * @param string|null $view View to use for rendering + * @param string|null $layout Layout to use + * @return \Cake\Http\Response A response object containing the rendered view. + * @link https://book.cakephp.org/3.0/en/controllers.html#rendering-a-view + */ + public function render($view = null, $layout = null) + { + $builder = $this->viewBuilder(); + if (!$builder->getTemplatePath()) { + $builder->setTemplatePath($this->_viewPath()); + } + + if ($this->request->getParam('bare')) { + $builder->enableAutoLayout(false); + } + $this->autoRender = false; + + $event = $this->dispatchEvent('Controller.beforeRender'); + if ($event->getResult() instanceof Response) { + return $event->getResult(); + } + if ($event->isStopped()) { + return $this->response; + } + + if ($builder->getTemplate() === null && $this->request->getParam('action')) { + $builder->setTemplate($this->request->getParam('action')); + } + + $this->View = $this->createView(); + $contents = $this->View->render($view, $layout); + $this->response = $this->View->response->withStringBody($contents); + + return $this->response; + } + + /** + * Get the viewPath based on controller name and request prefix. + * + * @return string + */ + protected function _viewPath() + { + $viewPath = $this->name; + if ($this->request->getParam('prefix')) { + $prefixes = array_map( + 'Cake\Utility\Inflector::camelize', + explode('/', $this->request->getParam('prefix')) + ); + $viewPath = implode(DIRECTORY_SEPARATOR, $prefixes) . DIRECTORY_SEPARATOR . $viewPath; + } + + return $viewPath; + } + + /** + * Returns the referring URL for this request. + * + * @param string|array|null $default Default URL to use if HTTP_REFERER cannot be read from headers + * @param bool $local If true, restrict referring URLs to local server + * @return string Referring URL + */ + public function referer($default = null, $local = false) + { + if (!$this->request) { + return Router::url($default, !$local); + } + + $referer = $this->request->referer($local); + if ($referer === '/' && $default && $default !== $referer) { + $url = Router::url($default, !$local); + $base = $this->request->getAttribute('base'); + if ($local && $base && strpos($url, $base) === 0) { + $url = substr($url, strlen($base)); + if ($url[0] !== '/') { + $url = '/' . $url; + } + + return $url; + } + + return $url; + } + + return $referer; + } + + /** + * Handles pagination of records in Table objects. + * + * Will load the referenced Table object, and have the PaginatorComponent + * paginate the query using the request date and settings defined in `$this->paginate`. + * + * This method will also make the PaginatorHelper available in the view. + * + * @param \Cake\ORM\Table|string|\Cake\ORM\Query|null $object Table to paginate + * (e.g: Table instance, 'TableName' or a Query object) + * @param array $settings The settings/configuration used for pagination. + * @return \Cake\ORM\ResultSet|\Cake\Datasource\ResultSetInterface Query results + * @link https://book.cakephp.org/3.0/en/controllers.html#paginating-a-model + * @throws \RuntimeException When no compatible table object can be found. + */ + public function paginate($object = null, array $settings = []) + { + if (is_object($object)) { + $table = $object; + } + + if (is_string($object) || $object === null) { + $try = [$object, $this->modelClass]; + foreach ($try as $tableName) { + if (empty($tableName)) { + continue; + } + $table = $this->loadModel($tableName); + break; + } + } + + $this->loadComponent('Paginator'); + if (empty($table)) { + throw new RuntimeException('Unable to locate an object compatible with paginate.'); + } + $settings += $this->paginate; + + return $this->Paginator->paginate($table, $settings); + } + + /** + * Method to check that an action is accessible from a URL. + * + * Override this method to change which controller methods can be reached. + * The default implementation disallows access to all methods defined on Cake\Controller\Controller, + * and allows all public methods on all subclasses of this class. + * + * @param string $action The action to check. + * @return bool Whether or not the method is accessible from a URL. + * @throws \ReflectionException + */ + public function isAction($action) + { + $baseClass = new ReflectionClass('Cake\Controller\Controller'); + if ($baseClass->hasMethod($action)) { + return false; + } + try { + $method = new ReflectionMethod($this, $action); + } catch (ReflectionException $e) { + return false; + } + + return $method->isPublic(); + } + + /** + * Called before the controller action. You can use this method to configure and customize components + * or perform logic that needs to happen before each controller action. + * + * @param \Cake\Event\Event $event An Event instance + * @return \Cake\Http\Response|null + * @link https://book.cakephp.org/3.0/en/controllers.html#request-life-cycle-callbacks + */ + public function beforeFilter(Event $event) + { + return null; + } + + /** + * Called after the controller action is run, but before the view is rendered. You can use this method + * to perform logic or set view variables that are required on every request. + * + * @param \Cake\Event\Event $event An Event instance + * @return \Cake\Http\Response|null + * @link https://book.cakephp.org/3.0/en/controllers.html#request-life-cycle-callbacks + */ + public function beforeRender(Event $event) + { + return null; + } + + /** + * The beforeRedirect method is invoked when the controller's redirect method is called but before any + * further action. + * + * If the event is stopped the controller will not continue on to redirect the request. + * The $url and $status variables have same meaning as for the controller's method. + * You can set the event result to response instance or modify the redirect location + * using controller's response instance. + * + * @param \Cake\Event\Event $event An Event instance + * @param string|array $url A string or array-based URL pointing to another location within the app, + * or an absolute URL + * @param \Cake\Http\Response $response The response object. + * @return \Cake\Http\Response|null + * @link https://book.cakephp.org/3.0/en/controllers.html#request-life-cycle-callbacks + */ + public function beforeRedirect(Event $event, $url, Response $response) + { + return null; + } + + /** + * Called after the controller action is run and rendered. + * + * @param \Cake\Event\Event $event An Event instance + * @return \Cake\Http\Response|null + * @link https://book.cakephp.org/3.0/en/controllers.html#request-life-cycle-callbacks + */ + public function afterFilter(Event $event) + { + return null; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/ErrorController.php b/app/vendor/cakephp/cakephp/src/Controller/ErrorController.php new file mode 100644 index 000000000..51b52957a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/ErrorController.php @@ -0,0 +1,47 @@ +loadComponent('RequestHandler'); + } + + /** + * beforeRender callback. + * + * @param \Cake\Event\Event $event Event. + * @return void + */ + public function beforeRender(Event $event) + { + $this->viewBuilder()->setTemplatePath('Error'); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Controller/Exception/AuthSecurityException.php b/app/vendor/cakephp/cakephp/src/Controller/Exception/AuthSecurityException.php new file mode 100644 index 000000000..f902d5443 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Controller/Exception/AuthSecurityException.php @@ -0,0 +1,25 @@ +_type; + } + + /** + * Set Message + * + * @param string $message Exception message + * @return void + */ + public function setMessage($message) + { + $this->message = $message; + } + + /** + * Set Reason + * + * @param string|null $reason Reason details + * @return void + */ + public function setReason($reason = null) + { + $this->_reason = $reason; + } + + /** + * Get Reason + * + * @return string + */ + public function getReason() + { + return $this->_reason; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/App.php b/app/vendor/cakephp/cakephp/src/Core/App.php new file mode 100644 index 000000000..b18d09c9c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/App.php @@ -0,0 +1,214 @@ +{"{$key}Enabled"} = (bool)$options[$key]; + } + } + foreach (['name', 'path', 'classPath', 'configPath'] as $path) { + if (isset($options[$path])) { + $this->{$path} = $options[$path]; + } + } + + $this->initialize(); + } + + /** + * {@inheritdoc} + */ + public function initialize() + { + } + + /** + * {@inheritDoc} + */ + public function getName() + { + if ($this->name) { + return $this->name; + } + $parts = explode('\\', get_class($this)); + array_pop($parts); + $this->name = implode('/', $parts); + + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getPath() + { + if ($this->path) { + return $this->path; + } + $reflection = new ReflectionClass($this); + $path = dirname($reflection->getFileName()); + + // Trim off src + if (substr($path, -3) === 'src') { + $path = substr($path, 0, -3); + } + $this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + + return $this->path; + } + + /** + * {@inheritDoc} + */ + public function getConfigPath() + { + if ($this->configPath) { + return $this->configPath; + } + $path = $this->getPath(); + + return $path . 'config' . DIRECTORY_SEPARATOR; + } + + /** + * {@inheritDoc} + */ + public function getClassPath() + { + if ($this->classPath) { + return $this->classPath; + } + $path = $this->getPath(); + + return $path . 'src' . DIRECTORY_SEPARATOR; + } + + /** + * {@inheritdoc} + */ + public function enable($hook) + { + $this->checkHook($hook); + $this->{"{$hook}Enabled}"} = true; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function disable($hook) + { + $this->checkHook($hook); + $this->{"{$hook}Enabled"} = false; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isEnabled($hook) + { + $this->checkHook($hook); + + return $this->{"{$hook}Enabled"} === true; + } + + /** + * Check if a hook name is valid + * + * @param string $hook The hook name to check + * @throws \InvalidArgumentException on invalid hooks + * @return void + */ + protected function checkHook($hook) + { + if (!in_array($hook, static::VALID_HOOKS)) { + throw new InvalidArgumentException( + "`$hook` is not a valid hook name. Must be one of " . implode(', ', static::VALID_HOOKS) + ); + } + } + + /** + * {@inheritdoc} + */ + public function routes($routes) + { + $path = $this->getConfigPath() . 'routes.php'; + if (file_exists($path)) { + require $path; + } + } + + /** + * {@inheritdoc} + */ + public function bootstrap(PluginApplicationInterface $app) + { + $bootstrap = $this->getConfigPath() . 'bootstrap.php'; + if (file_exists($bootstrap)) { + require $bootstrap; + } + } + + /** + * {@inheritdoc} + */ + public function console($commands) + { + return $commands->addMany($commands->discoverPlugin($this->getName())); + } + + /** + * {@inheritdoc} + */ + public function middleware($middleware) + { + return $middleware; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/ClassLoader.php b/app/vendor/cakephp/cakephp/src/Core/ClassLoader.php new file mode 100644 index 000000000..2f5bdefc7 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/ClassLoader.php @@ -0,0 +1,136 @@ +_prefixes[$prefix])) { + $this->_prefixes[$prefix] = []; + } + + if ($prepend) { + array_unshift($this->_prefixes[$prefix], $baseDir); + } else { + $this->_prefixes[$prefix][] = $baseDir; + } + } + + /** + * Loads the class file for a given class name. + * + * @param string $class The fully-qualified class name. + * @return string|false The mapped file name on success, or boolean false on + * failure. + */ + public function loadClass($class) + { + $prefix = $class; + + while (($pos = strrpos($prefix, '\\')) !== false) { + $prefix = substr($class, 0, $pos + 1); + $relativeClass = substr($class, $pos + 1); + + $mappedFile = $this->_loadMappedFile($prefix, $relativeClass); + if ($mappedFile) { + return $mappedFile; + } + + $prefix = rtrim($prefix, '\\'); + } + + return false; + } + + /** + * Load the mapped file for a namespace prefix and relative class. + * + * @param string $prefix The namespace prefix. + * @param string $relativeClass The relative class name. + * @return mixed Boolean false if no mapped file can be loaded, or the + * name of the mapped file that was loaded. + */ + protected function _loadMappedFile($prefix, $relativeClass) + { + if (!isset($this->_prefixes[$prefix])) { + return false; + } + + foreach ($this->_prefixes[$prefix] as $baseDir) { + $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php'; + + if ($this->_requireFile($file)) { + return $file; + } + } + + return false; + } + + /** + * If a file exists, require it from the file system. + * + * @param string $file The file to require. + * @return bool True if the file exists, false if not. + */ + protected function _requireFile($file) + { + if (file_exists($file)) { + require $file; + + return true; + } + + return false; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/Configure.php b/app/vendor/cakephp/cakephp/src/Core/Configure.php new file mode 100644 index 000000000..cb65453ce --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/Configure.php @@ -0,0 +1,463 @@ + false + ]; + + /** + * Configured engine classes, used to load config files from resources + * + * @see \Cake\Core\Configure::load() + * @var \Cake\Core\Configure\ConfigEngineInterface[] + */ + protected static $_engines = []; + + /** + * Flag to track whether or not ini_set exists. + * + * @var bool|null + */ + protected static $_hasIniSet; + + /** + * Used to store a dynamic variable in Configure. + * + * Usage: + * ``` + * Configure::write('One.key1', 'value of the Configure::One[key1]'); + * Configure::write(['One.key1' => 'value of the Configure::One[key1]']); + * Configure::write('One', [ + * 'key1' => 'value of the Configure::One[key1]', + * 'key2' => 'value of the Configure::One[key2]' + * ]); + * + * Configure::write([ + * 'One.key1' => 'value of the Configure::One[key1]', + * 'One.key2' => 'value of the Configure::One[key2]' + * ]); + * ``` + * + * @param string|array $config The key to write, can be a dot notation value. + * Alternatively can be an array containing key(s) and value(s). + * @param mixed $value Value to set for var + * @return bool True if write was successful + * @link https://book.cakephp.org/3.0/en/development/configuration.html#writing-configuration-data + */ + public static function write($config, $value = null) + { + if (!is_array($config)) { + $config = [$config => $value]; + } + + foreach ($config as $name => $value) { + static::$_values = Hash::insert(static::$_values, $name, $value); + } + + if (isset($config['debug'])) { + if (static::$_hasIniSet === null) { + static::$_hasIniSet = function_exists('ini_set'); + } + if (static::$_hasIniSet) { + ini_set('display_errors', $config['debug'] ? '1' : '0'); + } + } + + return true; + } + + /** + * Used to read information stored in Configure. It's not + * possible to store `null` values in Configure. + * + * Usage: + * ``` + * Configure::read('Name'); will return all values for Name + * Configure::read('Name.key'); will return only the value of Configure::Name[key] + * ``` + * + * @param string|null $var Variable to obtain. Use '.' to access array elements. + * @param mixed $default The return value when the configure does not exist + * @return mixed Value stored in configure, or null. + * @link https://book.cakephp.org/3.0/en/development/configuration.html#reading-configuration-data + */ + public static function read($var = null, $default = null) + { + if ($var === null) { + return static::$_values; + } + + return Hash::get(static::$_values, $var, $default); + } + + /** + * Returns true if given variable is set in Configure. + * + * @param string $var Variable name to check for + * @return bool True if variable is there + */ + public static function check($var) + { + if (empty($var)) { + return false; + } + + return static::read($var) !== null; + } + + /** + * Used to get information stored in Configure. It's not + * possible to store `null` values in Configure. + * + * Acts as a wrapper around Configure::read() and Configure::check(). + * The configure key/value pair fetched via this method is expected to exist. + * In case it does not an exception will be thrown. + * + * Usage: + * ``` + * Configure::readOrFail('Name'); will return all values for Name + * Configure::readOrFail('Name.key'); will return only the value of Configure::Name[key] + * ``` + * + * @param string $var Variable to obtain. Use '.' to access array elements. + * @return mixed Value stored in configure. + * @throws \RuntimeException if the requested configuration is not set. + * @link https://book.cakephp.org/3.0/en/development/configuration.html#reading-configuration-data + */ + public static function readOrFail($var) + { + if (static::check($var) === false) { + throw new RuntimeException(sprintf('Expected configuration key "%s" not found.', $var)); + } + + return static::read($var); + } + + /** + * Used to delete a variable from Configure. + * + * Usage: + * ``` + * Configure::delete('Name'); will delete the entire Configure::Name + * Configure::delete('Name.key'); will delete only the Configure::Name[key] + * ``` + * + * @param string $var the var to be deleted + * @return void + * @link https://book.cakephp.org/3.0/en/development/configuration.html#deleting-configuration-data + */ + public static function delete($var) + { + static::$_values = Hash::remove(static::$_values, $var); + } + + /** + * Used to consume information stored in Configure. It's not + * possible to store `null` values in Configure. + * + * Acts as a wrapper around Configure::consume() and Configure::check(). + * The configure key/value pair consumed via this method is expected to exist. + * In case it does not an exception will be thrown. + * + * @param string $var Variable to consume. Use '.' to access array elements. + * @return mixed Value stored in configure. + * @throws \RuntimeException if the requested configuration is not set. + * @since 3.6.0 + */ + public static function consumeOrFail($var) + { + if (static::check($var) === false) { + throw new RuntimeException(sprintf('Expected configuration key "%s" not found.', $var)); + } + + return static::consume($var); + } + + /** + * Used to read and delete a variable from Configure. + * + * This is primarily used during bootstrapping to move configuration data + * out of configure into the various other classes in CakePHP. + * + * @param string $var The key to read and remove. + * @return array|string|null + */ + public static function consume($var) + { + if (strpos($var, '.') === false) { + if (!isset(static::$_values[$var])) { + return null; + } + $value = static::$_values[$var]; + unset(static::$_values[$var]); + + return $value; + } + $value = Hash::get(static::$_values, $var); + static::delete($var); + + return $value; + } + + /** + * Add a new engine to Configure. Engines allow you to read configuration + * files in various formats/storage locations. CakePHP comes with two built-in engines + * PhpConfig and IniConfig. You can also implement your own engine classes in your application. + * + * To add a new engine to Configure: + * + * ``` + * Configure::config('ini', new IniConfig()); + * ``` + * + * @param string $name The name of the engine being configured. This alias is used later to + * read values from a specific engine. + * @param \Cake\Core\Configure\ConfigEngineInterface $engine The engine to append. + * @return void + */ + public static function config($name, ConfigEngineInterface $engine) + { + static::$_engines[$name] = $engine; + } + + /** + * Gets the names of the configured Engine objects. + * + * @param string|null $name Engine name. + * @return array|bool Array of the configured Engine objects, bool for specific name. + */ + public static function configured($name = null) + { + if ($name !== null) { + return isset(static::$_engines[$name]); + } + + return array_keys(static::$_engines); + } + + /** + * Remove a configured engine. This will unset the engine + * and make any future attempts to use it cause an Exception. + * + * @param string $name Name of the engine to drop. + * @return bool Success + */ + public static function drop($name) + { + if (!isset(static::$_engines[$name])) { + return false; + } + unset(static::$_engines[$name]); + + return true; + } + + /** + * Loads stored configuration information from a resource. You can add + * config file resource engines with `Configure::config()`. + * + * Loaded configuration information will be merged with the current + * runtime configuration. You can load configuration files from plugins + * by preceding the filename with the plugin name. + * + * `Configure::load('Users.user', 'default')` + * + * Would load the 'user' config file using the default config engine. You can load + * app config files by giving the name of the resource you want loaded. + * + * ``` + * Configure::load('setup', 'default'); + * ``` + * + * If using `default` config and no engine has been configured for it yet, + * one will be automatically created using PhpConfig + * + * @param string $key name of configuration resource to load. + * @param string $config Name of the configured engine to use to read the resource identified by $key. + * @param bool $merge if config files should be merged instead of simply overridden + * @return bool False if file not found, true if load successful. + * @link https://book.cakephp.org/3.0/en/development/configuration.html#reading-and-writing-configuration-files + */ + public static function load($key, $config = 'default', $merge = true) + { + $engine = static::_getEngine($config); + if (!$engine) { + return false; + } + $values = $engine->read($key); + + if ($merge) { + $values = Hash::merge(static::$_values, $values); + } + + return static::write($values); + } + + /** + * Dump data currently in Configure into $key. The serialization format + * is decided by the config engine attached as $config. For example, if the + * 'default' adapter is a PhpConfig, the generated file will be a PHP + * configuration file loadable by the PhpConfig. + * + * ### Usage + * + * Given that the 'default' engine is an instance of PhpConfig. + * Save all data in Configure to the file `my_config.php`: + * + * ``` + * Configure::dump('my_config', 'default'); + * ``` + * + * Save only the error handling configuration: + * + * ``` + * Configure::dump('error', 'default', ['Error', 'Exception']; + * ``` + * + * @param string $key The identifier to create in the config adapter. + * This could be a filename or a cache key depending on the adapter being used. + * @param string $config The name of the configured adapter to dump data with. + * @param array $keys The name of the top-level keys you want to dump. + * This allows you save only some data stored in Configure. + * @return bool Success + * @throws \Cake\Core\Exception\Exception if the adapter does not implement a `dump` method. + */ + public static function dump($key, $config = 'default', $keys = []) + { + $engine = static::_getEngine($config); + if (!$engine) { + throw new Exception(sprintf('There is no "%s" config engine.', $config)); + } + $values = static::$_values; + if (!empty($keys) && is_array($keys)) { + $values = array_intersect_key($values, array_flip($keys)); + } + + return (bool)$engine->dump($key, $values); + } + + /** + * Get the configured engine. Internally used by `Configure::load()` and `Configure::dump()` + * Will create new PhpConfig for default if not configured yet. + * + * @param string $config The name of the configured adapter + * @return \Cake\Core\Configure\ConfigEngineInterface|false Engine instance or false + */ + protected static function _getEngine($config) + { + if (!isset(static::$_engines[$config])) { + if ($config !== 'default') { + return false; + } + static::config($config, new PhpConfig()); + } + + return static::$_engines[$config]; + } + + /** + * Used to determine the current version of CakePHP. + * + * Usage + * ``` + * Configure::version(); + * ``` + * + * @return string Current version of CakePHP + */ + public static function version() + { + if (!isset(static::$_values['Cake']['version'])) { + $config = require CORE_PATH . 'config/config.php'; + static::write($config); + } + + return static::$_values['Cake']['version']; + } + + /** + * Used to write runtime configuration into Cache. Stored runtime configuration can be + * restored using `Configure::restore()`. These methods can be used to enable configuration managers + * frontends, or other GUI type interfaces for configuration. + * + * @param string $name The storage name for the saved configuration. + * @param string $cacheConfig The cache configuration to save into. Defaults to 'default' + * @param array|null $data Either an array of data to store, or leave empty to store all values. + * @return bool Success + */ + public static function store($name, $cacheConfig = 'default', $data = null) + { + if ($data === null) { + $data = static::$_values; + } + + return Cache::write($name, $data, $cacheConfig); + } + + /** + * Restores configuration data stored in the Cache into configure. Restored + * values will overwrite existing ones. + * + * @param string $name Name of the stored config file to load. + * @param string $cacheConfig Name of the Cache configuration to read from. + * @return bool Success. + */ + public static function restore($name, $cacheConfig = 'default') + { + $values = Cache::read($name, $cacheConfig); + if ($values) { + return static::write($values); + } + + return false; + } + + /** + * Clear all values stored in Configure. + * + * @return bool success. + */ + public static function clear() + { + static::$_values = []; + + return true; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/Configure/ConfigEngineInterface.php b/app/vendor/cakephp/cakephp/src/Core/Configure/ConfigEngineInterface.php new file mode 100644 index 000000000..188c0b9ad --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/Configure/ConfigEngineInterface.php @@ -0,0 +1,43 @@ + ['password' => 'secret']]` + * + * You can nest properties as deeply as needed using `.`'s. In addition to using `.` you + * can use standard ini section notation to create nested structures: + * + * ``` + * [section] + * key = value + * ``` + * + * Once loaded into Configure, the above would be accessed using: + * + * `Configure::read('section.key'); + * + * You can also use `.` separated values in section names to create more deeply + * nested structures. + * + * IniConfig also manipulates how the special ini values of + * 'yes', 'no', 'on', 'off', 'null' are handled. These values will be + * converted to their boolean equivalents. + * + * @see https://secure.php.net/parse_ini_file + */ +class IniConfig implements ConfigEngineInterface +{ + + use FileConfigTrait; + + /** + * File extension. + * + * @var string + */ + protected $_extension = '.ini'; + + /** + * The section to read, if null all sections will be read. + * + * @var string|null + */ + protected $_section; + + /** + * Build and construct a new ini file parser. The parser can be used to read + * ini files that are on the filesystem. + * + * @param string|null $path Path to load ini config files from. Defaults to CONFIG. + * @param string|null $section Only get one section, leave null to parse and fetch + * all sections in the ini file. + */ + public function __construct($path = null, $section = null) + { + if ($path === null) { + $path = CONFIG; + } + $this->_path = $path; + $this->_section = $section; + } + + /** + * Read an ini file and return the results as an array. + * + * @param string $key The identifier to read from. If the key has a . it will be treated + * as a plugin prefix. The chosen file must be on the engine's path. + * @return array Parsed configuration values. + * @throws \Cake\Core\Exception\Exception when files don't exist. + * Or when files contain '..' as this could lead to abusive reads. + */ + public function read($key) + { + $file = $this->_getFilePath($key, true); + + $contents = parse_ini_file($file, true); + if ($this->_section && isset($contents[$this->_section])) { + $values = $this->_parseNestedValues($contents[$this->_section]); + } else { + $values = []; + foreach ($contents as $section => $attribs) { + if (is_array($attribs)) { + $values[$section] = $this->_parseNestedValues($attribs); + } else { + $parse = $this->_parseNestedValues([$attribs]); + $values[$section] = array_shift($parse); + } + } + } + + return $values; + } + + /** + * parses nested values out of keys. + * + * @param array $values Values to be exploded. + * @return array Array of values exploded + */ + protected function _parseNestedValues($values) + { + foreach ($values as $key => $value) { + if ($value === '1') { + $value = true; + } + if ($value === '') { + $value = false; + } + unset($values[$key]); + if (strpos($key, '.') !== false) { + $values = Hash::insert($values, $key, $value); + } else { + $values[$key] = $value; + } + } + + return $values; + } + + /** + * Dumps the state of Configure data into an ini formatted string. + * + * @param string $key The identifier to write to. If the key has a . it will be treated + * as a plugin prefix. + * @param array $data The data to convert to ini file. + * @return bool Success. + */ + public function dump($key, array $data) + { + $result = []; + foreach ($data as $k => $value) { + $isSection = false; + if ($k[0] !== '[') { + $result[] = "[$k]"; + $isSection = true; + } + if (is_array($value)) { + $kValues = Hash::flatten($value, '.'); + foreach ($kValues as $k2 => $v) { + $result[] = "$k2 = " . $this->_value($v); + } + } + if ($isSection) { + $result[] = ''; + } + } + $contents = trim(implode("\n", $result)); + + $filename = $this->_getFilePath($key); + + return file_put_contents($filename, $contents) > 0; + } + + /** + * Converts a value into the ini equivalent + * + * @param mixed $value Value to export. + * @return string String value for ini file. + */ + protected function _value($value) + { + if ($value === null) { + return 'null'; + } + if ($value === true) { + return 'true'; + } + if ($value === false) { + return 'false'; + } + + return (string)$value; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/Configure/Engine/JsonConfig.php b/app/vendor/cakephp/cakephp/src/Core/Configure/Engine/JsonConfig.php new file mode 100644 index 000000000..b45434793 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/Configure/Engine/JsonConfig.php @@ -0,0 +1,114 @@ +_path = $path; + } + + /** + * Read a config file and return its contents. + * + * Files with `.` in the name will be treated as values in plugins. Instead of + * reading from the initialized path, plugin keys will be located using Plugin::path(). + * + * @param string $key The identifier to read from. If the key has a . it will be treated + * as a plugin prefix. + * @return array Parsed configuration values. + * @throws \Cake\Core\Exception\Exception When files don't exist or when + * files contain '..' (as this could lead to abusive reads) or when there + * is an error parsing the JSON string. + */ + public function read($key) + { + $file = $this->_getFilePath($key, true); + + $values = json_decode(file_get_contents($file), true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception(sprintf( + 'Error parsing JSON string fetched from config file "%s.json": %s', + $key, + json_last_error_msg() + )); + } + if (!is_array($values)) { + throw new Exception(sprintf( + 'Decoding JSON config file "%s.json" did not return an array', + $key + )); + } + + return $values; + } + + /** + * Converts the provided $data into a JSON string that can be used saved + * into a file and loaded later. + * + * @param string $key The identifier to write to. If the key has a . it will + * be treated as a plugin prefix. + * @param array $data Data to dump. + * @return bool Success + */ + public function dump($key, array $data) + { + $filename = $this->_getFilePath($key); + + return file_put_contents($filename, json_encode($data, JSON_PRETTY_PRINT)) > 0; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/Configure/Engine/PhpConfig.php b/app/vendor/cakephp/cakephp/src/Core/Configure/Engine/PhpConfig.php new file mode 100644 index 000000000..ef4cae491 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/Configure/Engine/PhpConfig.php @@ -0,0 +1,121 @@ + 0, + * 'Security' => [ + * 'salt' => 'its-secret' + * ], + * 'App' => [ + * 'namespace' => 'App' + * ] + * ]; + * ``` + * + * @see Cake\Core\Configure::load() for how to load custom configuration files. + */ +class PhpConfig implements ConfigEngineInterface +{ + + use FileConfigTrait; + + /** + * File extension. + * + * @var string + */ + protected $_extension = '.php'; + + /** + * Constructor for PHP Config file reading. + * + * @param string|null $path The path to read config files from. Defaults to CONFIG. + */ + public function __construct($path = null) + { + if ($path === null) { + $path = CONFIG; + } + $this->_path = $path; + } + + /** + * Read a config file and return its contents. + * + * Files with `.` in the name will be treated as values in plugins. Instead of + * reading from the initialized path, plugin keys will be located using Plugin::path(). + * + * Setting a `$config` variable is deprecated. Use `return` instead. + * + * @param string $key The identifier to read from. If the key has a . it will be treated + * as a plugin prefix. + * @return array Parsed configuration values. + * @throws \Cake\Core\Exception\Exception when files don't exist or they don't contain `$config`. + * Or when files contain '..' as this could lead to abusive reads. + */ + public function read($key) + { + $file = $this->_getFilePath($key, true); + + $return = include $file; + if (is_array($return)) { + return $return; + } + + if (!isset($config)) { + throw new Exception(sprintf('Config file "%s" did not return an array', $key . '.php')); + } + deprecationWarning(sprintf( + 'PHP configuration files like "%s" should not set `$config`. Instead return an array.', + $key . '.php' + )); + + return $config; + } + + /** + * Converts the provided $data into a string of PHP code that can + * be used saved into a file and loaded later. + * + * @param string $key The identifier to write to. If the key has a . it will be treated + * as a plugin prefix. + * @param array $data Data to dump. + * @return bool Success + */ + public function dump($key, array $data) + { + $contents = '_getFilePath($key); + + return file_put_contents($filename, $contents) > 0; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/Configure/FileConfigTrait.php b/app/vendor/cakephp/cakephp/src/Core/Configure/FileConfigTrait.php new file mode 100644 index 000000000..b7ddeee42 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/Configure/FileConfigTrait.php @@ -0,0 +1,70 @@ +_path . $key; + } + + $file .= $this->_extension; + + if (!$checkExists || is_file($file)) { + return $file; + } + + $realPath = realpath($file); + if ($realPath !== false && is_file($realPath)) { + return $realPath; + } + + throw new Exception(sprintf('Could not load configuration file: %s', $file)); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/ConsoleApplicationInterface.php b/app/vendor/cakephp/cakephp/src/Core/ConsoleApplicationInterface.php new file mode 100644 index 000000000..04e2f2488 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/ConsoleApplicationInterface.php @@ -0,0 +1,38 @@ +_defaultCode; + } + + if (is_array($message)) { + $this->_attributes = $message; + $message = vsprintf($this->_messageTemplate, $message); + } + parent::__construct($message, $code, $previous); + } + + /** + * Get the passed in attributes + * + * @return array + */ + public function getAttributes() + { + return $this->_attributes; + } + + /** + * Get/set the response header to be used + * + * See also Cake\Http\Response::withHeader() + * + * @param string|array|null $header An array of header strings or a single header string + * - an associative array of "header name" => "header value" + * - an array of string headers is also accepted (deprecated) + * @param string|null $value The header value. + * @return array + */ + public function responseHeader($header = null, $value = null) + { + if ($header === null) { + return $this->_responseHeaders; + } + if (is_array($header)) { + if (isset($header[0])) { + deprecationWarning( + 'Passing a list string headers to Exception::responseHeader() is deprecated. ' . + 'Use an associative array instead.' + ); + } + + return $this->_responseHeaders = $header; + } + $this->_responseHeaders = [$header => $value]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/Exception/MissingPluginException.php b/app/vendor/cakephp/cakephp/src/Core/Exception/MissingPluginException.php new file mode 100644 index 000000000..bb865ac03 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/Exception/MissingPluginException.php @@ -0,0 +1,22 @@ +setConfig('key', $value); + * ``` + * + * Setting a nested value: + * + * ``` + * $this->setConfig('some.nested.key', $value); + * ``` + * + * Updating multiple config settings at the same time: + * + * ``` + * $this->setConfig(['one' => 'value', 'another' => 'value']); + * ``` + * + * @param string|array $key The key to set, or a complete array of configs. + * @param mixed|null $value The value to set. + * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true. + * @return $this + * @throws \Cake\Core\Exception\Exception When trying to set a key that is invalid. + */ + public function setConfig($key, $value = null, $merge = true) + { + if (!$this->_configInitialized) { + $this->_config = $this->_defaultConfig; + $this->_configInitialized = true; + } + + $this->_configWrite($key, $value, $merge); + + return $this; + } + + /** + * Returns the config. + * + * ### Usage + * + * Reading the whole config: + * + * ``` + * $this->getConfig(); + * ``` + * + * Reading a specific value: + * + * ``` + * $this->getConfig('key'); + * ``` + * + * Reading a nested value: + * + * ``` + * $this->getConfig('some.nested.key'); + * ``` + * + * Reading with default value: + * + * ``` + * $this->getConfig('some-key', 'default-value'); + * ``` + * + * @param string|null $key The key to get or null for the whole config. + * @param mixed $default The return value when the key does not exist. + * @return mixed Config value being read. + */ + public function getConfig($key = null, $default = null) + { + if (!$this->_configInitialized) { + $this->_config = $this->_defaultConfig; + $this->_configInitialized = true; + } + + $return = $this->_configRead($key); + + return $return === null ? $default : $return; + } + + /** + * Gets/Sets the config. + * + * ### Usage + * + * Reading the whole config: + * + * ``` + * $this->config(); + * ``` + * + * Reading a specific value: + * + * ``` + * $this->config('key'); + * ``` + * + * Reading a nested value: + * + * ``` + * $this->config('some.nested.key'); + * ``` + * + * Setting a specific value: + * + * ``` + * $this->config('key', $value); + * ``` + * + * Setting a nested value: + * + * ``` + * $this->config('some.nested.key', $value); + * ``` + * + * Updating multiple config settings at the same time: + * + * ``` + * $this->config(['one' => 'value', 'another' => 'value']); + * ``` + * + * @deprecated 3.4.0 use setConfig()/getConfig() instead. + * @param string|array|null $key The key to get/set, or a complete array of configs. + * @param mixed|null $value The value to set. + * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true. + * @return mixed Config value being read, or the object itself on write operations. + * @throws \Cake\Core\Exception\Exception When trying to set a key that is invalid. + */ + public function config($key = null, $value = null, $merge = true) + { + deprecationWarning( + get_called_class() . '::config() is deprecated. ' . + 'Use setConfig()/getConfig() instead.' + ); + + if (is_array($key) || func_num_args() >= 2) { + return $this->setConfig($key, $value, $merge); + } + + return $this->getConfig($key); + } + + /** + * Merge provided config with existing config. Unlike `config()` which does + * a recursive merge for nested keys, this method does a simple merge. + * + * Setting a specific value: + * + * ``` + * $this->configShallow('key', $value); + * ``` + * + * Setting a nested value: + * + * ``` + * $this->configShallow('some.nested.key', $value); + * ``` + * + * Updating multiple config settings at the same time: + * + * ``` + * $this->configShallow(['one' => 'value', 'another' => 'value']); + * ``` + * + * @param string|array $key The key to set, or a complete array of configs. + * @param mixed|null $value The value to set. + * @return $this + */ + public function configShallow($key, $value = null) + { + if (!$this->_configInitialized) { + $this->_config = $this->_defaultConfig; + $this->_configInitialized = true; + } + + $this->_configWrite($key, $value, 'shallow'); + + return $this; + } + + /** + * Reads a config key. + * + * @param string|null $key Key to read. + * @return mixed + */ + protected function _configRead($key) + { + if ($key === null) { + return $this->_config; + } + + if (strpos($key, '.') === false) { + return isset($this->_config[$key]) ? $this->_config[$key] : null; + } + + $return = $this->_config; + + foreach (explode('.', $key) as $k) { + if (!is_array($return) || !isset($return[$k])) { + $return = null; + break; + } + + $return = $return[$k]; + } + + return $return; + } + + /** + * Writes a config key. + * + * @param string|array $key Key to write to. + * @param mixed $value Value to write. + * @param bool|string $merge True to merge recursively, 'shallow' for simple merge, + * false to overwrite, defaults to false. + * @return void + * @throws \Cake\Core\Exception\Exception if attempting to clobber existing config + */ + protected function _configWrite($key, $value, $merge = false) + { + if (is_string($key) && $value === null) { + $this->_configDelete($key); + + return; + } + + if ($merge) { + $update = is_array($key) ? $key : [$key => $value]; + if ($merge === 'shallow') { + $this->_config = array_merge($this->_config, Hash::expand($update)); + } else { + $this->_config = Hash::merge($this->_config, Hash::expand($update)); + } + + return; + } + + if (is_array($key)) { + foreach ($key as $k => $val) { + $this->_configWrite($k, $val); + } + + return; + } + + if (strpos($key, '.') === false) { + $this->_config[$key] = $value; + + return; + } + + $update =& $this->_config; + $stack = explode('.', $key); + + foreach ($stack as $k) { + if (!is_array($update)) { + throw new Exception(sprintf('Cannot set %s value', $key)); + } + + if (!isset($update[$k])) { + $update[$k] = []; + } + + $update =& $update[$k]; + } + + $update = $value; + } + + /** + * Deletes a single config key. + * + * @param string $key Key to delete. + * @return void + * @throws \Cake\Core\Exception\Exception if attempting to clobber existing config + */ + protected function _configDelete($key) + { + if (strpos($key, '.') === false) { + unset($this->_config[$key]); + + return; + } + + $update =& $this->_config; + $stack = explode('.', $key); + $length = count($stack); + + foreach ($stack as $i => $k) { + if (!is_array($update)) { + throw new Exception(sprintf('Cannot unset %s value', $key)); + } + + if (!isset($update[$k])) { + break; + } + + if ($i === $length - 1) { + unset($update[$k]); + break; + } + + $update =& $update[$k]; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Core/LICENSE.txt new file mode 100644 index 000000000..0c4b7932c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2016, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/vendor/cakephp/cakephp/src/Core/ObjectRegistry.php b/app/vendor/cakephp/cakephp/src/Core/ObjectRegistry.php new file mode 100644 index 000000000..2b72bb9b0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/ObjectRegistry.php @@ -0,0 +1,392 @@ + [ + * 'className' => '\App\Controller\Component\AliasedEmailComponent' + * ]; + * ]; + * ``` + * + * All calls to the `Email` component would use `AliasedEmail` instead. + * + * @param string $objectName The name/class of the object to load. + * @param array $config Additional settings to use when loading the object. + * @return mixed + * @throws \Exception If the class cannot be found. + */ + public function load($objectName, $config = []) + { + if (is_array($config) && isset($config['className'])) { + $name = $objectName; + $objectName = $config['className']; + } else { + list(, $name) = pluginSplit($objectName); + } + + $loaded = isset($this->_loaded[$name]); + if ($loaded && !empty($config)) { + $this->_checkDuplicate($name, $config); + } + if ($loaded) { + return $this->_loaded[$name]; + } + + $className = $this->_resolveClassName($objectName); + if (!$className || (is_string($className) && !class_exists($className))) { + list($plugin, $objectName) = pluginSplit($objectName); + $this->_throwMissingClassError($objectName, $plugin); + } + $instance = $this->_create($className, $name, $config); + $this->_loaded[$name] = $instance; + + return $instance; + } + + /** + * Check for duplicate object loading. + * + * If a duplicate is being loaded and has different configuration, that is + * bad and an exception will be raised. + * + * An exception is raised, as replacing the object will not update any + * references other objects may have. Additionally, simply updating the runtime + * configuration is not a good option as we may be missing important constructor + * logic dependent on the configuration. + * + * @param string $name The name of the alias in the registry. + * @param array $config The config data for the new instance. + * @return void + * @throws \RuntimeException When a duplicate is found. + */ + protected function _checkDuplicate($name, $config) + { + /** @var \Cake\Core\InstanceConfigTrait $existing */ + $existing = $this->_loaded[$name]; + $msg = sprintf('The "%s" alias has already been loaded', $name); + $hasConfig = method_exists($existing, 'config'); + if (!$hasConfig) { + throw new RuntimeException($msg); + } + if (empty($config)) { + return; + } + $existingConfig = $existing->getConfig(); + unset($config['enabled'], $existingConfig['enabled']); + + $fail = false; + foreach ($config as $key => $value) { + if (!array_key_exists($key, $existingConfig)) { + $fail = true; + break; + } + if (isset($existingConfig[$key]) && $existingConfig[$key] !== $value) { + $fail = true; + break; + } + } + if ($fail) { + $msg .= ' with the following config: '; + $msg .= var_export($existingConfig, true); + $msg .= ' which differs from ' . var_export($config, true); + throw new RuntimeException($msg); + } + } + + /** + * Should resolve the classname for a given object type. + * + * @param string $class The class to resolve. + * @return string|bool The resolved name or false for failure. + */ + abstract protected function _resolveClassName($class); + + /** + * Throw an exception when the requested object name is missing. + * + * @param string $class The class that is missing. + * @param string $plugin The plugin $class is missing from. + * @return void + * @throws \Exception + */ + abstract protected function _throwMissingClassError($class, $plugin); + + /** + * Create an instance of a given classname. + * + * This method should construct and do any other initialization logic + * required. + * + * @param string $class The class to build. + * @param string $alias The alias of the object. + * @param array $config The Configuration settings for construction + * @return mixed + */ + abstract protected function _create($class, $alias, $config); + + /** + * Get the list of loaded objects. + * + * @return array List of object names. + */ + public function loaded() + { + return array_keys($this->_loaded); + } + + /** + * Check whether or not a given object is loaded. + * + * @param string $name The object name to check for. + * @return bool True is object is loaded else false. + */ + public function has($name) + { + return isset($this->_loaded[$name]); + } + + /** + * Get loaded object instance. + * + * @param string $name Name of object. + * @return object|null Object instance if loaded else null. + */ + public function get($name) + { + if (isset($this->_loaded[$name])) { + return $this->_loaded[$name]; + } + + return null; + } + + /** + * Provide public read access to the loaded objects + * + * @param string $name Name of property to read + * @return mixed + */ + public function __get($name) + { + return $this->get($name); + } + + /** + * Provide isset access to _loaded + * + * @param string $name Name of object being checked. + * @return bool + */ + public function __isset($name) + { + return isset($this->_loaded[$name]); + } + + /** + * Sets an object. + * + * @param string $name Name of a property to set. + * @param mixed $object Object to set. + * @return void + */ + public function __set($name, $object) + { + $this->set($name, $object); + } + + /** + * Unsets an object. + * + * @param string $name Name of a property to unset. + * @return void + */ + public function __unset($name) + { + $this->unload($name); + } + + /** + * Normalizes an object array, creates an array that makes lazy loading + * easier + * + * @param array $objects Array of child objects to normalize. + * @return array Array of normalized objects. + */ + public function normalizeArray($objects) + { + $normal = []; + foreach ($objects as $i => $objectName) { + $config = []; + if (!is_int($i)) { + $config = (array)$objectName; + $objectName = $i; + } + list(, $name) = pluginSplit($objectName); + if (isset($config['class'])) { + $normal[$name] = $config; + } else { + $normal[$name] = ['class' => $objectName, 'config' => $config]; + } + } + + return $normal; + } + + /** + * Clear loaded instances in the registry. + * + * If the registry subclass has an event manager, the objects will be detached from events as well. + * + * @return $this + */ + public function reset() + { + foreach (array_keys($this->_loaded) as $name) { + $this->unload($name); + } + + return $this; + } + + /** + * Set an object directly into the registry by name. + * + * If this collection implements events, the passed object will + * be attached into the event manager + * + * @param string $objectName The name of the object to set in the registry. + * @param object $object instance to store in the registry + * @return $this + */ + public function set($objectName, $object) + { + list(, $name) = pluginSplit($objectName); + + // Just call unload if the object was loaded before + if (array_key_exists($objectName, $this->_loaded)) { + $this->unload($objectName); + } + if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) { + $this->getEventManager()->on($object); + } + $this->_loaded[$name] = $object; + + return $this; + } + + /** + * Remove an object from the registry. + * + * If this registry has an event manager, the object will be detached from any events as well. + * + * @param string $objectName The name of the object to remove from the registry. + * @return $this + */ + public function unload($objectName) + { + if (empty($this->_loaded[$objectName])) { + list($plugin, $objectName) = pluginSplit($objectName); + $this->_throwMissingClassError($objectName, $plugin); + } + + $object = $this->_loaded[$objectName]; + if ($this instanceof EventDispatcherInterface && $object instanceof EventListenerInterface) { + $this->getEventManager()->off($object); + } + unset($this->_loaded[$objectName]); + + return $this; + } + + /** + * Returns an array iterator. + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->_loaded); + } + + /** + * Returns the number of loaded objects. + * + * @return int + */ + public function count() + { + return count($this->_loaded); + } + + /** + * Debug friendly object properties. + * + * @return array + */ + public function __debugInfo() + { + $properties = get_object_vars($this); + if (isset($properties['_loaded'])) { + $properties['_loaded'] = array_keys($properties['_loaded']); + } + + return $properties; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/Plugin.php b/app/vendor/cakephp/cakephp/src/Core/Plugin.php new file mode 100644 index 000000000..70f426298 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/Plugin.php @@ -0,0 +1,393 @@ + true, 'routes' => true])` + * + * Will load the bootstrap.php and routes.php files. + * + * `Plugin::load('DebugKit', ['bootstrap' => false, 'routes' => true])` + * + * Will load routes.php file but not bootstrap.php + * + * `Plugin::load('FOC/Authenticate')` + * + * Will load plugin from `plugins/FOC/Authenticate`. + * + * It is also possible to load multiple plugins at once. Examples: + * + * `Plugin::load(['DebugKit', 'ApiGenerator'])` + * + * Will load the DebugKit and ApiGenerator plugins. + * + * `Plugin::load(['DebugKit', 'ApiGenerator'], ['bootstrap' => true])` + * + * Will load bootstrap file for both plugins + * + * ``` + * Plugin::load([ + * 'DebugKit' => ['routes' => true], + * 'ApiGenerator' + * ], + * ['bootstrap' => true]) + * ``` + * + * Will only load the bootstrap for ApiGenerator and only the routes for DebugKit + * + * ### Configuration options + * + * - `bootstrap` - array - Whether or not you want the $plugin/config/bootstrap.php file loaded. + * - `routes` - boolean - Whether or not you want to load the $plugin/config/routes.php file. + * - `ignoreMissing` - boolean - Set to true to ignore missing bootstrap/routes files. + * - `path` - string - The path the plugin can be found on. If empty the default plugin path (App.pluginPaths) will be used. + * - `classBase` - The path relative to `path` which contains the folders with class files. + * Defaults to "src". + * - `autoload` - boolean - Whether or not you want an autoloader registered. This defaults to false. The framework + * assumes you have configured autoloaders using composer. However, if your application source tree is made up of + * plugins, this can be a useful option. + * + * @param string|array $plugin name of the plugin to be loaded in CamelCase format or array or plugins to load + * @param array $config configuration options for the plugin + * @throws \Cake\Core\Exception\MissingPluginException if the folder for the plugin to be loaded is not found + * @return void + */ + public static function load($plugin, array $config = []) + { + if (is_array($plugin)) { + foreach ($plugin as $name => $conf) { + list($name, $conf) = is_numeric($name) ? [$conf, $config] : [$name, $conf]; + static::load($name, $conf); + } + + return; + } + + $config += [ + 'autoload' => false, + 'bootstrap' => false, + 'routes' => false, + 'console' => true, + 'classBase' => 'src', + 'ignoreMissing' => false, + 'name' => $plugin + ]; + + if (!isset($config['path'])) { + $config['path'] = static::getCollection()->findPath($plugin); + } + + $config['classPath'] = $config['path'] . $config['classBase'] . DIRECTORY_SEPARATOR; + if (!isset($config['configPath'])) { + $config['configPath'] = $config['path'] . 'config' . DIRECTORY_SEPARATOR; + } + $pluginClass = str_replace('/', '\\', $plugin) . '\\Plugin'; + if (class_exists($pluginClass)) { + $instance = new $pluginClass($config); + } else { + // Use stub plugin as this method will be removed long term. + $instance = new BasePlugin($config); + } + static::getCollection()->add($instance); + + if ($config['autoload'] === true) { + if (empty(static::$_loader)) { + static::$_loader = new ClassLoader(); + static::$_loader->register(); + } + static::$_loader->addNamespace( + str_replace('/', '\\', $plugin), + $config['path'] . $config['classBase'] . DIRECTORY_SEPARATOR + ); + static::$_loader->addNamespace( + str_replace('/', '\\', $plugin) . '\Test', + $config['path'] . 'tests' . DIRECTORY_SEPARATOR + ); + } + + if ($config['bootstrap'] === true) { + static::bootstrap($plugin); + } + } + + /** + * Will load all the plugins located in the default plugin folder. + * + * If passed an options array, it will be used as a common default for all plugins to be loaded + * It is possible to set specific defaults for each plugins in the options array. Examples: + * + * ``` + * Plugin::loadAll([ + * ['bootstrap' => true], + * 'DebugKit' => ['routes' => true], + * ]); + * ``` + * + * The above example will load the bootstrap file for all plugins, but for DebugKit it will only load the routes file + * and will not look for any bootstrap script. + * + * If a plugin has been loaded already, it will not be reloaded by loadAll(). + * + * @param array $options Options. + * @return void + * @throws \Cake\Core\Exception\MissingPluginException + */ + public static function loadAll(array $options = []) + { + $plugins = []; + foreach (App::path('Plugin') as $path) { + if (!is_dir($path)) { + continue; + } + $dir = new DirectoryIterator($path); + foreach ($dir as $dirPath) { + if ($dirPath->isDir() && !$dirPath->isDot()) { + $plugins[] = $dirPath->getBasename(); + } + } + } + if (Configure::check('plugins')) { + $plugins = array_merge($plugins, array_keys(Configure::read('plugins'))); + $plugins = array_unique($plugins); + } + + $collection = static::getCollection(); + foreach ($plugins as $p) { + $opts = isset($options[$p]) ? $options[$p] : null; + if ($opts === null && isset($options[0])) { + $opts = $options[0]; + } + if ($collection->has($p)) { + continue; + } + static::load($p, (array)$opts); + } + } + + /** + * Returns the filesystem path for a plugin + * + * @param string $name name of the plugin in CamelCase format + * @return string path to the plugin folder + * @throws \Cake\Core\Exception\MissingPluginException if the folder for plugin was not found or plugin has not been loaded + */ + public static function path($name) + { + $plugin = static::getCollection()->get($name); + + return $plugin->getPath(); + } + + /** + * Returns the filesystem path for plugin's folder containing class folders. + * + * @param string $name name of the plugin in CamelCase format. + * @return string Path to the plugin folder container class folders. + * @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded. + */ + public static function classPath($name) + { + $plugin = static::getCollection()->get($name); + + return $plugin->getClassPath(); + } + + /** + * Returns the filesystem path for plugin's folder containing config files. + * + * @param string $name name of the plugin in CamelCase format. + * @return string Path to the plugin folder container config files. + * @throws \Cake\Core\Exception\MissingPluginException If plugin has not been loaded. + */ + public static function configPath($name) + { + $plugin = static::getCollection()->get($name); + + return $plugin->getConfigPath(); + } + + /** + * Loads the bootstrapping files for a plugin, or calls the initialization setup in the configuration + * + * @param string $name name of the plugin + * @return mixed + * @see \Cake\Core\Plugin::load() for examples of bootstrap configuration + */ + public static function bootstrap($name) + { + $plugin = static::getCollection()->get($name); + if (!$plugin->isEnabled('bootstrap')) { + return false; + } + // Disable bootstrapping for this plugin as it will have + // been bootstrapped. + $plugin->disable('bootstrap'); + + return static::_includeFile( + $plugin->getConfigPath() . 'bootstrap.php', + true + ); + } + + /** + * Loads the routes file for a plugin, or all plugins configured to load their respective routes file. + * + * If you need fine grained control over how routes are loaded for plugins, you + * can use {@see Cake\Routing\RouteBuilder::loadPlugin()} + * + * @param string|null $name name of the plugin, if null will operate on all + * plugins having enabled the loading of routes files. + * @return bool + * @deprecated 3.6.5 This method is no longer needed when using HttpApplicationInterface based applications. + * This method will be removed in 4.0.0 + */ + public static function routes($name = null) + { + deprecationWarning( + 'You no longer need to call `Plugin::routes()` after upgrading to use Http\Server. ' . + 'See https://book.cakephp.org/3.0/en/development/application.html#adding-the-new-http-stack-to-an-existing-application ' . + 'for upgrade information.' + ); + if ($name === null) { + foreach (static::loaded() as $p) { + static::routes($p); + } + + return true; + } + $plugin = static::getCollection()->get($name); + if (!$plugin->isEnabled('routes')) { + return false; + } + + return (bool)static::_includeFile( + $plugin->getConfigPath() . 'routes.php', + true + ); + } + + /** + * Returns true if the plugin $plugin is already loaded + * If plugin is null, it will return a list of all loaded plugins + * + * @param string|null $plugin Plugin name. + * @return bool|array Boolean true if $plugin is already loaded. + * If $plugin is null, returns a list of plugins that have been loaded + */ + public static function loaded($plugin = null) + { + if ($plugin !== null) { + return static::getCollection()->has($plugin); + } + $names = []; + foreach (static::getCollection() as $plugin) { + $names[] = $plugin->getName(); + } + sort($names); + + return $names; + } + + /** + * Forgets a loaded plugin or all of them if first parameter is null + * + * @param string|null $plugin name of the plugin to forget + * @return void + */ + public static function unload($plugin = null) + { + if ($plugin === null) { + static::$plugins = null; + } else { + static::getCollection()->remove($plugin); + } + } + + /** + * Include file, ignoring include error if needed if file is missing + * + * @param string $file File to include + * @param bool $ignoreMissing Whether to ignore include error for missing files + * @return mixed + */ + protected static function _includeFile($file, $ignoreMissing = false) + { + if ($ignoreMissing && !is_file($file)) { + return false; + } + + return include $file; + } + + /** + * Get the shared plugin collection. + * + * @internal + * @return \Cake\Core\PluginCollection + */ + public static function getCollection() + { + if (!isset(static::$plugins)) { + static::$plugins = new PluginCollection(); + } + + return static::$plugins; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/PluginApplicationInterface.php b/app/vendor/cakephp/cakephp/src/Core/PluginApplicationInterface.php new file mode 100644 index 000000000..f8d20caff --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/PluginApplicationInterface.php @@ -0,0 +1,70 @@ +add($plugin); + } + $this->loadConfig(); + } + + /** + * Load the path information stored in vendor/cakephp-plugins.php + * + * This file is generated by the cakephp/plugin-installer package and used + * to locate plugins on the filesystem as applications can use `extra.plugin-paths` + * in their composer.json file to move plugin outside of vendor/ + * + * @internal + * @return void + */ + protected function loadConfig() + { + if (Configure::check('plugins')) { + return; + } + $vendorFile = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php'; + if (!file_exists($vendorFile)) { + $vendorFile = dirname(dirname(dirname(dirname(__DIR__)))) . DIRECTORY_SEPARATOR . 'cakephp-plugins.php'; + if (!file_exists($vendorFile)) { + Configure::write(['plugins' => []]); + + return; + } + } + + $config = require $vendorFile; + Configure::write($config); + } + + /** + * Locate a plugin path by looking at configuration data. + * + * This will use the `plugins` Configure key, and fallback to enumerating `App::path('Plugin')` + * + * This method is not part of the official public API as plugins with + * no plugin class are being phased out. + * + * @param string $name The plugin name to locate a path for. Will return '' when a plugin cannot be found. + * @return string + * @throws Cake\Core\Exception\MissingPluginException when a plugin path cannot be resolved. + * @internal + */ + public function findPath($name) + { + $this->loadConfig(); + + $path = Configure::read('plugins.' . $name); + if ($path) { + return $path; + } + + $pluginPath = str_replace('/', DIRECTORY_SEPARATOR, $name); + $paths = App::path('Plugin'); + foreach ($paths as $path) { + if (is_dir($path . $pluginPath)) { + return $path . $pluginPath . DIRECTORY_SEPARATOR; + } + } + + throw new MissingPluginException(['plugin' => $name]); + } + + /** + * Add a plugin to the collection + * + * Plugins will be keyed by their names. + * + * @param \Cake\Core\PluginInterface $plugin The plugin to load. + * @return $this + */ + public function add(PluginInterface $plugin) + { + $name = $plugin->getName(); + $this->plugins[$name] = $plugin; + $this->names = array_keys($this->plugins); + + return $this; + } + + /** + * Remove a plugin from the collection if it exists. + * + * @param string $name The named plugin. + * @return $this + */ + public function remove($name) + { + unset($this->plugins[$name]); + $this->names = array_keys($this->plugins); + + return $this; + } + + /** + * Check whether the named plugin exists in the collection. + * + * @param string $name The named plugin. + * @return bool + */ + public function has($name) + { + return isset($this->plugins[$name]); + } + + /** + * Get the a plugin by name + * + * @param string $name The plugin to get. + * @return \Cake\Core\PluginInterface The plugin. + * @throws \Cake\Core\Exception\MissingPluginException when unknown plugins are fetched. + */ + public function get($name) + { + if (!$this->has($name)) { + throw new MissingPluginException(['plugin' => $name]); + } + + return $this->plugins[$name]; + } + + /** + * Part of Iterator Interface + * + * @return void + */ + public function next() + { + $this->position++; + } + + /** + * Part of Iterator Interface + * + * @return string + */ + public function key() + { + return $this->names[$this->position]; + } + + /** + * Part of Iterator Interface + * + * @return \Cake\Core\PluginInterface + */ + public function current() + { + $name = $this->names[$this->position]; + + return $this->plugins[$name]; + } + + /** + * Part of Iterator Interface + * + * @return void + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Part of Iterator Interface + * + * @return bool + */ + public function valid() + { + return $this->position < count($this->plugins); + } + + /** + * Implementation of Countable. + * + * Get the number of plugins in the collection. + * + * @return int + */ + public function count() + { + return count($this->plugins); + } + + /** + * Filter the plugins to those with the named hook enabled. + * + * @param string $hook The hook to filter plugins by + * @return \Generator A generator containing matching plugins. + * @throws \InvalidArgumentException on invalid hooks + */ + public function with($hook) + { + if (!in_array($hook, PluginInterface::VALID_HOOKS)) { + throw new InvalidArgumentException("The `{$hook}` hook is not a known plugin hook."); + } + foreach ($this as $plugin) { + if ($plugin->isEnabled($hook)) { + yield $plugin; + } + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/PluginInterface.php b/app/vendor/cakephp/cakephp/src/Core/PluginInterface.php new file mode 100644 index 000000000..3f4bd214f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/PluginInterface.php @@ -0,0 +1,117 @@ +strategy = $strategy; + $this->retries = $retries; + } + + /** + * The number of retries to perform in case of failure + * + * @param callable $action The callable action to execute with a retry strategy + * @return mixed The return value of the passed action callable + * @throws \Exception + */ + public function run(callable $action) + { + $retryCount = 0; + $lastException = null; + + do { + try { + return $action(); + } catch (Exception $e) { + $lastException = $e; + if (!$this->strategy->shouldRetry($e, $retryCount)) { + throw $e; + } + } + } while ($this->retries > $retryCount++); + + if ($lastException !== null) { + throw $lastException; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/Retry/RetryStrategyInterface.php b/app/vendor/cakephp/cakephp/src/Core/Retry/RetryStrategyInterface.php new file mode 100644 index 000000000..2e0f95380 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/Retry/RetryStrategyInterface.php @@ -0,0 +1,33 @@ + configuration data for adapter. + * @throws \BadMethodCallException When trying to modify an existing config. + * @throws \LogicException When trying to store an invalid structured config array. + * @return void + */ + public static function setConfig($key, $config = null) + { + if ($config === null) { + if (!is_array($key)) { + throw new LogicException('If config is null, key must be an array.'); + } + foreach ($key as $name => $settings) { + static::setConfig($name, $settings); + } + + return; + } + + if (isset(static::$_config[$key])) { + throw new BadMethodCallException(sprintf('Cannot reconfigure existing key "%s"', $key)); + } + + if (is_object($config)) { + $config = ['className' => $config]; + } + + if (isset($config['url'])) { + $parsed = static::parseDsn($config['url']); + unset($config['url']); + $config = $parsed + $config; + } + + if (isset($config['engine']) && empty($config['className'])) { + $config['className'] = $config['engine']; + unset($config['engine']); + } + static::$_config[$key] = $config; + } + + /** + * Reads existing configuration. + * + * @param string $key The name of the configuration. + * @return array|null Array of configuration data. + */ + public static function getConfig($key) + { + return isset(static::$_config[$key]) ? static::$_config[$key] : null; + } + + /** + * This method can be used to define configuration adapters for an application + * or read existing configuration. + * + * To change an adapter's configuration at runtime, first drop the adapter and then + * reconfigure it. + * + * Adapters will not be constructed until the first operation is done. + * + * ### Usage + * + * Assuming that the class' name is `Cache` the following scenarios + * are supported: + * + * Reading config data back: + * + * ``` + * Cache::config('default'); + * ``` + * + * Setting a cache engine up. + * + * ``` + * Cache::config('default', $settings); + * ``` + * + * Injecting a constructed adapter in: + * + * ``` + * Cache::config('default', $instance); + * ``` + * + * Configure multiple adapters at once: + * + * ``` + * Cache::config($arrayOfConfig); + * ``` + * + * @deprecated 3.4.0 Use setConfig()/getConfig() instead. + * @param string|array $key The name of the configuration, or an array of multiple configs. + * @param array|null $config An array of name => configuration data for adapter. + * @return array|null Null when adding configuration or an array of configuration data when reading. + * @throws \BadMethodCallException When trying to modify an existing config. + */ + public static function config($key, $config = null) + { + deprecationWarning( + get_called_class() . '::config() is deprecated. ' . + 'Use setConfig()/getConfig() instead.' + ); + + if ($config !== null || is_array($key)) { + static::setConfig($key, $config); + + return null; + } + + return static::getConfig($key); + } + + /** + * Drops a constructed adapter. + * + * If you wish to modify an existing configuration, you should drop it, + * change configuration and then re-add it. + * + * If the implementing objects supports a `$_registry` object the named configuration + * will also be unloaded from the registry. + * + * @param string $config An existing configuration you wish to remove. + * @return bool Success of the removal, returns false when the config does not exist. + */ + public static function drop($config) + { + if (!isset(static::$_config[$config])) { + return false; + } + if (isset(static::$_registry)) { + static::$_registry->unload($config); + } + unset(static::$_config[$config]); + + return true; + } + + /** + * Returns an array containing the named configurations + * + * @return array Array of configurations. + */ + public static function configured() + { + return array_keys(static::$_config); + } + + /** + * Parses a DSN into a valid connection configuration + * + * This method allows setting a DSN using formatting similar to that used by PEAR::DB. + * The following is an example of its usage: + * + * ``` + * $dsn = 'mysql://user:pass@localhost/database?'; + * $config = ConnectionManager::parseDsn($dsn); + * + * $dsn = 'Cake\Log\Engine\FileLog://?types=notice,info,debug&file=debug&path=LOGS'; + * $config = Log::parseDsn($dsn); + * + * $dsn = 'smtp://user:secret@localhost:25?timeout=30&client=null&tls=null'; + * $config = Email::parseDsn($dsn); + * + * $dsn = 'file:///?className=\My\Cache\Engine\FileEngine'; + * $config = Cache::parseDsn($dsn); + * + * $dsn = 'File://?prefix=myapp_cake_core_&serialize=true&duration=+2 minutes&path=/tmp/persistent/'; + * $config = Cache::parseDsn($dsn); + * ``` + * + * For all classes, the value of `scheme` is set as the value of both the `className` + * unless they have been otherwise specified. + * + * Note that querystring arguments are also parsed and set as values in the returned configuration. + * + * @param string $dsn The DSN string to convert to a configuration array + * @return array The configuration array to be stored after parsing the DSN + * @throws \InvalidArgumentException If not passed a string, or passed an invalid string + */ + public static function parseDsn($dsn) + { + if (empty($dsn)) { + return []; + } + + if (!is_string($dsn)) { + throw new InvalidArgumentException('Only strings can be passed to parseDsn'); + } + + $pattern = <<<'REGEXP' +{ + ^ + (?P<_scheme> + (?P[\w\\\\]+):// + ) + (?P<_username> + (?P.*?) + (?P<_password> + :(?P.*?) + )? + @ + )? + (?P<_host> + (?P[^?#/:@]+) + (?P<_port> + :(?P\d+) + )? + )? + (?P<_path> + (?P/[^?#]*) + )? + (?P<_query> + \?(?P[^#]*) + )? + (?P<_fragment> + \#(?P.*) + )? + $ +}x +REGEXP; + + preg_match($pattern, $dsn, $parsed); + + if (!$parsed) { + throw new InvalidArgumentException("The DSN string '{$dsn}' could not be parsed."); + } + + $exists = []; + foreach ($parsed as $k => $v) { + if (is_int($k)) { + unset($parsed[$k]); + } elseif (strpos($k, '_') === 0) { + $exists[substr($k, 1)] = ($v !== ''); + unset($parsed[$k]); + } elseif ($v === '' && !$exists[$k]) { + unset($parsed[$k]); + } + } + + $query = ''; + + if (isset($parsed['query'])) { + $query = $parsed['query']; + unset($parsed['query']); + } + + parse_str($query, $queryArgs); + + foreach ($queryArgs as $key => $value) { + if ($value === 'true') { + $queryArgs[$key] = true; + } elseif ($value === 'false') { + $queryArgs[$key] = false; + } elseif ($value === 'null') { + $queryArgs[$key] = null; + } + } + + $parsed = $queryArgs + $parsed; + + if (empty($parsed['className'])) { + $classMap = static::getDsnClassMap(); + + $parsed['className'] = $parsed['scheme']; + if (isset($classMap[$parsed['scheme']])) { + $parsed['className'] = $classMap[$parsed['scheme']]; + } + } + + return $parsed; + } + + /** + * Updates the DSN class map for this class. + * + * @param array $map Additions/edits to the class map to apply. + * @return void + */ + public static function setDsnClassMap(array $map) + { + static::$_dsnClassMap = $map + static::$_dsnClassMap; + } + + /** + * Returns the DSN class map for this class. + * + * @return array + */ + public static function getDsnClassMap() + { + return static::$_dsnClassMap; + } + + /** + * Returns or updates the DSN class map for this class. + * + * @deprecated 3.4.0 Use setDsnClassMap()/getDsnClassMap() instead. + * @param array|null $map Additions/edits to the class map to apply. + * @return array + */ + public static function dsnClassMap(array $map = null) + { + deprecationWarning( + get_called_class() . '::setDsnClassMap() is deprecated. ' . + 'Use setDsnClassMap()/getDsnClassMap() instead.' + ); + + if ($map !== null) { + static::setDsnClassMap($map); + } + + return static::getDsnClassMap(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/composer.json b/app/vendor/cakephp/cakephp/src/Core/composer.json new file mode 100644 index 000000000..3a485859c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/composer.json @@ -0,0 +1,39 @@ +{ + "name": "cakephp/core", + "description": "CakePHP Framework Core classes", + "type": "library", + "keywords": [ + "cakephp", + "framework", + "core" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/core/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/core" + }, + "require": { + "php": ">=5.6.0", + "cakephp/utility": "^3.6.0" + }, + "suggest": { + "cakephp/event": "To use PluginApplicationInterface or plugin applications." + }, + "autoload": { + "psr-4": { + "Cake\\Core\\": "." + }, + "files": [ + "functions.php" + ] + } +} diff --git a/app/vendor/cakephp/cakephp/src/Core/functions.php b/app/vendor/cakephp/cakephp/src/Core/functions.php new file mode 100644 index 000000000..b2ccfd7c7 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Core/functions.php @@ -0,0 +1,325 @@ + $t) { + $texts[$k] = h($t, $double, $charset); + } + + return $texts; + } elseif (is_object($text)) { + if (method_exists($text, '__toString')) { + $text = (string)$text; + } else { + $text = '(object)' . get_class($text); + } + } elseif ($text === null || is_scalar($text)) { + return $text; + } + + static $defaultCharset = false; + if ($defaultCharset === false) { + $defaultCharset = mb_internal_encoding(); + if ($defaultCharset === null) { + $defaultCharset = 'UTF-8'; + } + } + if (is_string($double)) { + deprecationWarning( + 'Passing charset string for 2nd argument is deprecated. ' . + 'Use the 3rd argument instead.' + ); + $charset = $double; + } + + return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, $charset ?: $defaultCharset, $double); + } + +} + +if (!function_exists('pluginSplit')) { + /** + * Splits a dot syntax plugin name into its plugin and class name. + * If $name does not have a dot, then index 0 will be null. + * + * Commonly used like + * ``` + * list($plugin, $name) = pluginSplit($name); + * ``` + * + * @param string $name The name you want to plugin split. + * @param bool $dotAppend Set to true if you want the plugin to have a '.' appended to it. + * @param string|null $plugin Optional default plugin to use if no plugin is found. Defaults to null. + * @return array Array with 2 indexes. 0 => plugin name, 1 => class name. + * @link https://book.cakephp.org/3.0/en/core-libraries/global-constants-and-functions.html#pluginSplit + */ + function pluginSplit($name, $dotAppend = false, $plugin = null) + { + if (strpos($name, '.') !== false) { + $parts = explode('.', $name, 2); + if ($dotAppend) { + $parts[0] .= '.'; + } + + return $parts; + } + + return [$plugin, $name]; + } + +} + +if (!function_exists('namespaceSplit')) { + /** + * Split the namespace from the classname. + * + * Commonly used like `list($namespace, $className) = namespaceSplit($class);`. + * + * @param string $class The full class name, ie `Cake\Core\App`. + * @return array Array with 2 indexes. 0 => namespace, 1 => classname. + */ + function namespaceSplit($class) + { + $pos = strrpos($class, '\\'); + if ($pos === false) { + return ['', $class]; + } + + return [substr($class, 0, $pos), substr($class, $pos + 1)]; + } + +} + +if (!function_exists('pr')) { + /** + * print_r() convenience function. + * + * In terminals this will act similar to using print_r() directly, when not run on cli + * print_r() will also wrap
 tags around the output of given variable. Similar to debug().
+     *
+     * This function returns the same variable that was passed.
+     *
+     * @param mixed $var Variable to print out.
+     * @return mixed the same $var that was passed to this function
+     * @link https://book.cakephp.org/3.0/en/core-libraries/global-constants-and-functions.html#pr
+     * @see debug()
+     */
+    function pr($var)
+    {
+        if (!Configure::read('debug')) {
+            return $var;
+        }
+
+        $template = (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') ? '
%s
' : "\n%s\n\n"; + printf($template, trim(print_r($var, true))); + + return $var; + } + +} + +if (!function_exists('pj')) { + /** + * json pretty print convenience function. + * + * In terminals this will act similar to using json_encode() with JSON_PRETTY_PRINT directly, when not run on cli + * will also wrap
 tags around the output of given variable. Similar to pr().
+     *
+     * This function returns the same variable that was passed.
+     *
+     * @param mixed $var Variable to print out.
+     * @return mixed the same $var that was passed to this function
+     * @see pr()
+     * @link https://book.cakephp.org/3.0/en/core-libraries/global-constants-and-functions.html#pj
+     */
+    function pj($var)
+    {
+        if (!Configure::read('debug')) {
+            return $var;
+        }
+
+        $template = (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') ? '
%s
' : "\n%s\n\n"; + printf($template, trim(json_encode($var, JSON_PRETTY_PRINT))); + + return $var; + } + +} + +if (!function_exists('env')) { + /** + * Gets an environment variable from available sources, and provides emulation + * for unsupported or inconsistent environment variables (i.e. DOCUMENT_ROOT on + * IIS, or SCRIPT_NAME in CGI mode). Also exposes some additional custom + * environment information. + * + * @param string $key Environment variable name. + * @param string|null $default Specify a default value in case the environment variable is not defined. + * @return string|bool|null Environment variable setting. + * @link https://book.cakephp.org/3.0/en/core-libraries/global-constants-and-functions.html#env + */ + function env($key, $default = null) + { + if ($key === 'HTTPS') { + if (isset($_SERVER['HTTPS'])) { + return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); + } + + return (strpos((string)env('SCRIPT_URI'), 'https://') === 0); + } + + if ($key === 'SCRIPT_NAME' && env('CGI_MODE') && isset($_ENV['SCRIPT_URL'])) { + $key = 'SCRIPT_URL'; + } + + $val = null; + if (isset($_SERVER[$key])) { + $val = $_SERVER[$key]; + } elseif (isset($_ENV[$key])) { + $val = $_ENV[$key]; + } elseif (getenv($key) !== false) { + $val = getenv($key); + } + + if ($key === 'REMOTE_ADDR' && $val === env('SERVER_ADDR')) { + $addr = env('HTTP_PC_REMOTE_ADDR'); + if ($addr !== null) { + $val = $addr; + } + } + + if ($val !== null) { + return $val; + } + + switch ($key) { + case 'DOCUMENT_ROOT': + $name = env('SCRIPT_NAME'); + $filename = env('SCRIPT_FILENAME'); + $offset = 0; + if (!strpos($name, '.php')) { + $offset = 4; + } + + return substr($filename, 0, -(strlen($name) + $offset)); + case 'PHP_SELF': + return str_replace(env('DOCUMENT_ROOT'), '', env('SCRIPT_FILENAME')); + case 'CGI_MODE': + return (PHP_SAPI === 'cgi'); + } + + return $default; + } + +} + +if (!function_exists('triggerWarning')) { + /** + * Triggers an E_USER_WARNING. + * + * @param string $message The warning message. + * @return void + */ + function triggerWarning($message) + { + $stackFrame = 1; + $trace = debug_backtrace(); + if (isset($trace[$stackFrame])) { + $frame = $trace[$stackFrame]; + $frame += ['file' => '[internal]', 'line' => '??']; + $message = sprintf( + '%s - %s, line: %s', + $message, + $frame['file'], + $frame['line'] + ); + } + trigger_error($message, E_USER_WARNING); + } +} + +if (!function_exists('deprecationWarning')) { + /** + * Helper method for outputting deprecation warnings + * + * @param string $message The message to output as a deprecation warning. + * @param int $stackFrame The stack frame to include in the error. Defaults to 1 + * as that should point to application/plugin code. + * @return void + */ + function deprecationWarning($message, $stackFrame = 1) + { + if (!(error_reporting() & E_USER_DEPRECATED)) { + return; + } + + $trace = debug_backtrace(); + if (isset($trace[$stackFrame])) { + $frame = $trace[$stackFrame]; + $frame += ['file' => '[internal]', 'line' => '??']; + + $message = sprintf( + '%s - %s, line: %s' . "\n" . + ' You can disable deprecation warnings by setting `Error.errorLevel` to' . + ' `E_ALL & ~E_USER_DEPRECATED` in your config/app.php.', + $message, + $frame['file'], + $frame['line'] + ); + } + + trigger_error($message, E_USER_DEPRECATED); + } +} + +if (!function_exists('getTypeName')) { + /** + * Returns the objects class or var type of it's not an object + * + * @param mixed $var Variable to check + * @return string Returns the class name or variable type + */ + function getTypeName($var) + { + return is_object($var) ? get_class($var) : gettype($var); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Connection.php b/app/vendor/cakephp/cakephp/src/Database/Connection.php new file mode 100644 index 000000000..62a741139 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Connection.php @@ -0,0 +1,960 @@ +_config = $config; + + $driver = ''; + if (!empty($config['driver'])) { + $driver = $config['driver']; + } + $this->setDriver($driver, $config); + + if (!empty($config['log'])) { + $this->logQueries($config['log']); + } + } + + /** + * Destructor + * + * Disconnects the driver to release the connection. + */ + public function __destruct() + { + if ($this->_transactionStarted && class_exists('Cake\Log\Log')) { + Log::warning('The connection is going to be closed but there is an active transaction.'); + } + } + + /** + * {@inheritDoc} + */ + public function config() + { + return $this->_config; + } + + /** + * {@inheritDoc} + */ + public function configName() + { + if (empty($this->_config['name'])) { + return ''; + } + + return $this->_config['name']; + } + + /** + * Sets the driver instance. If a string is passed it will be treated + * as a class name and will be instantiated. + * + * @param \Cake\Database\Driver|string $driver The driver instance to use. + * @param array $config Config for a new driver. + * @throws \Cake\Database\Exception\MissingDriverException When a driver class is missing. + * @throws \Cake\Database\Exception\MissingExtensionException When a driver's PHP extension is missing. + * @return $this + */ + public function setDriver($driver, $config = []) + { + if (is_string($driver)) { + $className = App::className($driver, 'Database/Driver'); + if (!$className || !class_exists($className)) { + throw new MissingDriverException(['driver' => $driver]); + } + $driver = new $className($config); + } + if (!$driver->enabled()) { + throw new MissingExtensionException(['driver' => get_class($driver)]); + } + + $this->_driver = $driver; + + return $this; + } + + /** + * Get the retry wrapper object that is allows recovery from server disconnects + * while performing certain database actions, such as executing a query. + * + * @return \Cake\Core\Retry\CommandRetry The retry wrapper + */ + public function getDisconnectRetry() + { + return new CommandRetry(new ReconnectStrategy($this)); + } + + /** + * Gets the driver instance. + * + * @return \Cake\Database\Driver + */ + public function getDriver() + { + return $this->_driver; + } + + /** + * Sets the driver instance. If a string is passed it will be treated + * as a class name and will be instantiated. + * + * If no params are passed it will return the current driver instance. + * + * @deprecated 3.4.0 Use setDriver()/getDriver() instead. + * @param \Cake\Database\Driver|string|null $driver The driver instance to use. + * @param array $config Either config for a new driver or null. + * @throws \Cake\Database\Exception\MissingDriverException When a driver class is missing. + * @throws \Cake\Database\Exception\MissingExtensionException When a driver's PHP extension is missing. + * @return \Cake\Database\Driver + */ + public function driver($driver = null, $config = []) + { + deprecationWarning('Connection::driver() is deprecated. Use Connection::setDriver()/getDriver() instead.'); + if ($driver !== null) { + $this->setDriver($driver, $config); + } + + return $this->getDriver(); + } + + /** + * Connects to the configured database. + * + * @throws \Cake\Database\Exception\MissingConnectionException if credentials are invalid. + * @return bool true, if the connection was already established or the attempt was successful. + */ + public function connect() + { + try { + return $this->_driver->connect(); + } catch (Exception $e) { + throw new MissingConnectionException(['reason' => $e->getMessage()], null, $e); + } + } + + /** + * Disconnects from database server. + * + * @return void + */ + public function disconnect() + { + $this->_driver->disconnect(); + } + + /** + * Returns whether connection to database server was already established. + * + * @return bool + */ + public function isConnected() + { + return $this->_driver->isConnected(); + } + + /** + * Prepares a SQL statement to be executed. + * + * @param string|\Cake\Database\Query $sql The SQL to convert into a prepared statement. + * @return \Cake\Database\StatementInterface + */ + public function prepare($sql) + { + return $this->getDisconnectRetry()->run(function () use ($sql) { + $statement = $this->_driver->prepare($sql); + + if ($this->_logQueries) { + $statement = $this->_newLogger($statement); + } + + return $statement; + }); + } + + /** + * Executes a query using $params for interpolating values and $types as a hint for each + * those params. + * + * @param string $query SQL to be executed and interpolated with $params + * @param array $params list or associative array of params to be interpolated in $query as values + * @param array $types list or associative array of types to be used for casting values in query + * @return \Cake\Database\StatementInterface executed statement + */ + public function execute($query, array $params = [], array $types = []) + { + return $this->getDisconnectRetry()->run(function () use ($query, $params, $types) { + if (!empty($params)) { + $statement = $this->prepare($query); + $statement->bind($params, $types); + $statement->execute(); + } else { + $statement = $this->query($query); + } + + return $statement; + }); + } + + /** + * Compiles a Query object into a SQL string according to the dialect for this + * connection's driver + * + * @param \Cake\Database\Query $query The query to be compiled + * @param \Cake\Database\ValueBinder $generator The placeholder generator to use + * @return string + */ + public function compileQuery(Query $query, ValueBinder $generator) + { + return $this->getDriver()->compileQuery($query, $generator)[1]; + } + + /** + * Executes the provided query after compiling it for the specific driver + * dialect and returns the executed Statement object. + * + * @param \Cake\Database\Query $query The query to be executed + * @return \Cake\Database\StatementInterface executed statement + */ + public function run(Query $query) + { + return $this->getDisconnectRetry()->run(function () use ($query) { + $statement = $this->prepare($query); + $query->getValueBinder()->attachTo($statement); + $statement->execute(); + + return $statement; + }); + } + + /** + * Executes a SQL statement and returns the Statement object as result. + * + * @param string $sql The SQL query to execute. + * @return \Cake\Database\StatementInterface + */ + public function query($sql) + { + return $this->getDisconnectRetry()->run(function () use ($sql) { + $statement = $this->prepare($sql); + $statement->execute(); + + return $statement; + }); + } + + /** + * Create a new Query instance for this connection. + * + * @return \Cake\Database\Query + */ + public function newQuery() + { + return new Query($this); + } + + /** + * Sets a Schema\Collection object for this connection. + * + * @param \Cake\Database\Schema\Collection $collection The schema collection object + * @return $this + */ + public function setSchemaCollection(SchemaCollection $collection) + { + $this->_schemaCollection = $collection; + + return $this; + } + + /** + * Gets a Schema\Collection object for this connection. + * + * @return \Cake\Database\Schema\Collection + */ + public function getSchemaCollection() + { + if ($this->_schemaCollection !== null) { + return $this->_schemaCollection; + } + + if (!empty($this->_config['cacheMetadata'])) { + return $this->_schemaCollection = new CachedCollection($this, $this->_config['cacheMetadata']); + } + + return $this->_schemaCollection = new SchemaCollection($this); + } + + /** + * Gets or sets a Schema\Collection object for this connection. + * + * @deprecated 3.4.0 Use setSchemaCollection()/getSchemaCollection() + * @param \Cake\Database\Schema\Collection|null $collection The schema collection object + * @return \Cake\Database\Schema\Collection + */ + public function schemaCollection(SchemaCollection $collection = null) + { + deprecationWarning( + 'Connection::schemaCollection() is deprecated. ' . + 'Use Connection::setSchemaCollection()/getSchemaCollection() instead.' + ); + if ($collection !== null) { + $this->setSchemaCollection($collection); + } + + return $this->getSchemaCollection(); + } + + /** + * Executes an INSERT query on the specified table. + * + * @param string $table the table to insert values in + * @param array $data values to be inserted + * @param array $types list of associative array containing the types to be used for casting + * @return \Cake\Database\StatementInterface + */ + public function insert($table, array $data, array $types = []) + { + return $this->getDisconnectRetry()->run(function () use ($table, $data, $types) { + $columns = array_keys($data); + + return $this->newQuery()->insert($columns, $types) + ->into($table) + ->values($data) + ->execute(); + }); + } + + /** + * Executes an UPDATE statement on the specified table. + * + * @param string $table the table to update rows from + * @param array $data values to be updated + * @param array $conditions conditions to be set for update statement + * @param array $types list of associative array containing the types to be used for casting + * @return \Cake\Database\StatementInterface + */ + public function update($table, array $data, array $conditions = [], $types = []) + { + return $this->getDisconnectRetry()->run(function () use ($table, $data, $conditions, $types) { + return $this->newQuery()->update($table) + ->set($data, $types) + ->where($conditions, $types) + ->execute(); + }); + } + + /** + * Executes a DELETE statement on the specified table. + * + * @param string $table the table to delete rows from + * @param array $conditions conditions to be set for delete statement + * @param array $types list of associative array containing the types to be used for casting + * @return \Cake\Database\StatementInterface + */ + public function delete($table, $conditions = [], $types = []) + { + return $this->getDisconnectRetry()->run(function () use ($table, $conditions, $types) { + return $this->newQuery()->delete($table) + ->where($conditions, $types) + ->execute(); + }); + } + + /** + * Starts a new transaction. + * + * @return void + */ + public function begin() + { + if (!$this->_transactionStarted) { + if ($this->_logQueries) { + $this->log('BEGIN'); + } + + $this->getDisconnectRetry()->run(function () { + $this->_driver->beginTransaction(); + }); + + $this->_transactionLevel = 0; + $this->_transactionStarted = true; + $this->nestedTransactionRollbackException = null; + + return; + } + + $this->_transactionLevel++; + if ($this->isSavePointsEnabled()) { + $this->createSavePoint((string)$this->_transactionLevel); + } + } + + /** + * Commits current transaction. + * + * @return bool true on success, false otherwise + */ + public function commit() + { + if (!$this->_transactionStarted) { + return false; + } + + if ($this->_transactionLevel === 0) { + if ($this->wasNestedTransactionRolledback()) { + $e = $this->nestedTransactionRollbackException; + $this->nestedTransactionRollbackException = null; + throw $e; + } + + $this->_transactionStarted = false; + $this->nestedTransactionRollbackException = null; + if ($this->_logQueries) { + $this->log('COMMIT'); + } + + return $this->_driver->commitTransaction(); + } + if ($this->isSavePointsEnabled()) { + $this->releaseSavePoint((string)$this->_transactionLevel); + } + + $this->_transactionLevel--; + + return true; + } + + /** + * Rollback current transaction. + * + * @param bool|null $toBeginning Whether or not the transaction should be rolled back to the + * beginning of it. Defaults to false if using savepoints, or true if not. + * @return bool + */ + public function rollback($toBeginning = null) + { + if (!$this->_transactionStarted) { + return false; + } + + $useSavePoint = $this->isSavePointsEnabled(); + if ($toBeginning === null) { + $toBeginning = !$useSavePoint; + } + if ($this->_transactionLevel === 0 || $toBeginning) { + $this->_transactionLevel = 0; + $this->_transactionStarted = false; + $this->nestedTransactionRollbackException = null; + if ($this->_logQueries) { + $this->log('ROLLBACK'); + } + $this->_driver->rollbackTransaction(); + + return true; + } + + $savePoint = $this->_transactionLevel--; + if ($useSavePoint) { + $this->rollbackSavepoint($savePoint); + } elseif ($this->nestedTransactionRollbackException === null) { + $this->nestedTransactionRollbackException = new NestedTransactionRollbackException(); + } + + return true; + } + + /** + * Enables/disables the usage of savepoints, enables only if driver the allows it. + * + * If you are trying to enable this feature, make sure you check the return value of this + * function to verify it was enabled successfully. + * + * ### Example: + * + * `$connection->enableSavePoints(true)` Returns true if drivers supports save points, false otherwise + * `$connection->enableSavePoints(false)` Disables usage of savepoints and returns false + * + * @param bool $enable Whether or not save points should be used. + * @return $this + */ + public function enableSavePoints($enable) + { + if ($enable === false) { + $this->_useSavePoints = false; + } else { + $this->_useSavePoints = $this->_driver->supportsSavePoints(); + } + + return $this; + } + + /** + * Returns whether this connection is using savepoints for nested transactions + * + * @return bool true if enabled, false otherwise + */ + public function isSavePointsEnabled() + { + return $this->_useSavePoints; + } + + /** + * Returns whether this connection is using savepoints for nested transactions + * If a boolean is passed as argument it will enable/disable the usage of savepoints + * only if driver the allows it. + * + * If you are trying to enable this feature, make sure you check the return value of this + * function to verify it was enabled successfully. + * + * ### Example: + * + * `$connection->useSavePoints(true)` Returns true if drivers supports save points, false otherwise + * `$connection->useSavePoints(false)` Disables usage of savepoints and returns false + * `$connection->useSavePoints()` Returns current status + * + * @deprecated 3.4.0 Use enableSavePoints()/isSavePointsEnabled() instead. + * @param bool|null $enable Whether or not save points should be used. + * @return bool true if enabled, false otherwise + */ + public function useSavePoints($enable = null) + { + deprecationWarning( + 'Connection::useSavePoints() is deprecated. ' . + 'Use Connection::enableSavePoints()/isSavePointsEnabled() instead.' + ); + if ($enable !== null) { + $this->enableSavePoints($enable); + } + + return $this->isSavePointsEnabled(); + } + + /** + * Creates a new save point for nested transactions. + * + * @param string $name The save point name. + * @return void + */ + public function createSavePoint($name) + { + $this->execute($this->_driver->savePointSQL($name))->closeCursor(); + } + + /** + * Releases a save point by its name. + * + * @param string $name The save point name. + * @return void + */ + public function releaseSavePoint($name) + { + $this->execute($this->_driver->releaseSavePointSQL($name))->closeCursor(); + } + + /** + * Rollback a save point by its name. + * + * @param string $name The save point name. + * @return void + */ + public function rollbackSavepoint($name) + { + $this->execute($this->_driver->rollbackSavePointSQL($name))->closeCursor(); + } + + /** + * Run driver specific SQL to disable foreign key checks. + * + * @return void + */ + public function disableForeignKeys() + { + $this->getDisconnectRetry()->run(function () { + $this->execute($this->_driver->disableForeignKeySQL())->closeCursor(); + }); + } + + /** + * Run driver specific SQL to enable foreign key checks. + * + * @return void + */ + public function enableForeignKeys() + { + $this->getDisconnectRetry()->run(function () { + $this->execute($this->_driver->enableForeignKeySQL())->closeCursor(); + }); + } + + /** + * Returns whether the driver supports adding or dropping constraints + * to already created tables. + * + * @return bool true if driver supports dynamic constraints + */ + public function supportsDynamicConstraints() + { + return $this->_driver->supportsDynamicConstraints(); + } + + /** + * {@inheritDoc} + * + * ### Example: + * + * ``` + * $connection->transactional(function ($connection) { + * $connection->newQuery()->delete('users')->execute(); + * }); + * ``` + */ + public function transactional(callable $callback) + { + $this->begin(); + + try { + $result = $callback($this); + } catch (Exception $e) { + $this->rollback(false); + throw $e; + } + + if ($result === false) { + $this->rollback(false); + + return false; + } + + try { + $this->commit(); + } catch (NestedTransactionRollbackException $e) { + $this->rollback(false); + throw $e; + } + + return $result; + } + + /** + * Returns whether some nested transaction has been already rolled back. + * + * @return bool + */ + protected function wasNestedTransactionRolledback() + { + return $this->nestedTransactionRollbackException instanceof NestedTransactionRollbackException; + } + + /** + * {@inheritDoc} + * + * ### Example: + * + * ``` + * $connection->disableConstraints(function ($connection) { + * $connection->newQuery()->delete('users')->execute(); + * }); + * ``` + */ + public function disableConstraints(callable $callback) + { + return $this->getDisconnectRetry()->run(function () use ($callback) { + $this->disableForeignKeys(); + + try { + $result = $callback($this); + } catch (Exception $e) { + $this->enableForeignKeys(); + throw $e; + } + + $this->enableForeignKeys(); + + return $result; + }); + } + + /** + * Checks if a transaction is running. + * + * @return bool True if a transaction is running else false. + */ + public function inTransaction() + { + return $this->_transactionStarted; + } + + /** + * Quotes value to be used safely in database query. + * + * @param mixed $value The value to quote. + * @param string|null $type Type to be used for determining kind of quoting to perform + * @return string Quoted value + */ + public function quote($value, $type = null) + { + list($value, $type) = $this->cast($value, $type); + + return $this->_driver->quote($value, $type); + } + + /** + * Checks if the driver supports quoting. + * + * @return bool + */ + public function supportsQuoting() + { + return $this->_driver->supportsQuoting(); + } + + /** + * Quotes a database identifier (a column name, table name, etc..) to + * be used safely in queries without the risk of using reserved words. + * + * @param string $identifier The identifier to quote. + * @return string + */ + public function quoteIdentifier($identifier) + { + return $this->_driver->quoteIdentifier($identifier); + } + + /** + * Enables or disables metadata caching for this connection + * + * Changing this setting will not modify existing schema collections objects. + * + * @param bool|string $cache Either boolean false to disable metadata caching, or + * true to use `_cake_model_` or the name of the cache config to use. + * @return void + */ + public function cacheMetadata($cache) + { + $this->_schemaCollection = null; + $this->_config['cacheMetadata'] = $cache; + } + + /** + * {@inheritDoc} + */ + public function logQueries($enable = null) + { + if ($enable === null) { + return $this->_logQueries; + } + $this->_logQueries = $enable; + } + + /** + * {@inheritDoc} + * + * @deprecated 3.5.0 Use getLogger() and setLogger() instead. + */ + public function logger($instance = null) + { + deprecationWarning( + 'Connection::logger() is deprecated. ' . + 'Use Connection::setLogger()/getLogger() instead.' + ); + if ($instance === null) { + return $this->getLogger(); + } + + $this->setLogger($instance); + } + + /** + * Sets a logger + * + * @param \Cake\Database\Log\QueryLogger $logger Logger object + * @return $this + */ + public function setLogger($logger) + { + $this->_logger = $logger; + + return $this; + } + + /** + * Gets the logger object + * + * @return \Cake\Database\Log\QueryLogger logger instance + */ + public function getLogger() + { + if ($this->_logger === null) { + $this->_logger = new QueryLogger(); + } + + return $this->_logger; + } + + /** + * Logs a Query string using the configured logger object. + * + * @param string $sql string to be logged + * @return void + */ + public function log($sql) + { + $query = new LoggedQuery(); + $query->query = $sql; + $this->getLogger()->log($query); + } + + /** + * Returns a new statement object that will log the activity + * for the passed original statement instance. + * + * @param \Cake\Database\StatementInterface $statement the instance to be decorated + * @return \Cake\Database\Log\LoggingStatement + */ + protected function _newLogger(StatementInterface $statement) + { + $log = new LoggingStatement($statement, $this->_driver); + $log->setLogger($this->getLogger()); + + return $log; + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + $secrets = [ + 'password' => '*****', + 'username' => '*****', + 'host' => '*****', + 'database' => '*****', + 'port' => '*****' + ]; + $replace = array_intersect_key($secrets, $this->_config); + $config = $replace + $this->_config; + + return [ + 'config' => $config, + 'driver' => $this->_driver, + 'transactionLevel' => $this->_transactionLevel, + 'transactionStarted' => $this->_transactionStarted, + 'useSavePoints' => $this->_useSavePoints, + 'logQueries' => $this->_logQueries, + 'logger' => $this->_logger + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Dialect/MysqlDialectTrait.php b/app/vendor/cakephp/cakephp/src/Database/Dialect/MysqlDialectTrait.php new file mode 100644 index 000000000..a6966903f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Dialect/MysqlDialectTrait.php @@ -0,0 +1,84 @@ +_schemaDialect) { + $this->_schemaDialect = new MysqlSchema($this); + } + + return $this->_schemaDialect; + } + + /** + * {@inheritDoc} + */ + public function disableForeignKeySQL() + { + return 'SET foreign_key_checks = 0'; + } + + /** + * {@inheritDoc} + */ + public function enableForeignKeySQL() + { + return 'SET foreign_key_checks = 1'; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Dialect/PostgresDialectTrait.php b/app/vendor/cakephp/cakephp/src/Database/Dialect/PostgresDialectTrait.php new file mode 100644 index 000000000..3f3e5f4bf --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Dialect/PostgresDialectTrait.php @@ -0,0 +1,189 @@ +clause('epilog')) { + $query->epilog('RETURNING *'); + } + + return $query; + } + + /** + * Returns a dictionary of expressions to be transformed when compiling a Query + * to SQL. Array keys are method names to be called in this class + * + * @return array + */ + protected function _expressionTranslators() + { + $namespace = 'Cake\Database\Expression'; + + return [ + $namespace . '\FunctionExpression' => '_transformFunctionExpression' + ]; + } + + /** + * Receives a FunctionExpression and changes it so that it conforms to this + * SQL dialect. + * + * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert + * to postgres SQL. + * @return void + */ + protected function _transformFunctionExpression(FunctionExpression $expression) + { + switch ($expression->getName()) { + case 'CONCAT': + // CONCAT function is expressed as exp1 || exp2 + $expression->setName('')->setConjunction(' ||'); + break; + case 'DATEDIFF': + $expression + ->setName('') + ->setConjunction('-') + ->iterateParts(function ($p) { + if (is_string($p)) { + $p = ['value' => [$p => 'literal'], 'type' => null]; + } else { + $p['value'] = [$p['value']]; + } + + return new FunctionExpression('DATE', $p['value'], [$p['type']]); + }); + break; + case 'CURRENT_DATE': + $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']); + $expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'date' => 'literal']); + break; + case 'CURRENT_TIME': + $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']); + $expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'time' => 'literal']); + break; + case 'NOW': + $expression->setName('LOCALTIMESTAMP')->add([' 0 ' => 'literal']); + break; + case 'DATE_ADD': + $expression + ->setName('') + ->setConjunction(' + INTERVAL') + ->iterateParts(function ($p, $key) { + if ($key === 1) { + $p = sprintf("'%s'", $p); + } + + return $p; + }); + break; + case 'DAYOFWEEK': + $expression + ->setName('EXTRACT') + ->setConjunction(' ') + ->add(['DOW FROM' => 'literal'], [], true) + ->add([') + (1' => 'literal']); // Postgres starts on index 0 but Sunday should be 1 + break; + } + } + + /** + * Get the schema dialect. + * + * Used by Cake\Database\Schema package to reflect schema and + * generate schema. + * + * @return \Cake\Database\Schema\PostgresSchema + */ + public function schemaDialect() + { + if (!$this->_schemaDialect) { + $this->_schemaDialect = new PostgresSchema($this); + } + + return $this->_schemaDialect; + } + + /** + * {@inheritDoc} + */ + public function disableForeignKeySQL() + { + return 'SET CONSTRAINTS ALL DEFERRED'; + } + + /** + * {@inheritDoc} + */ + public function enableForeignKeySQL() + { + return 'SET CONSTRAINTS ALL IMMEDIATE'; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Dialect/SqliteDialectTrait.php b/app/vendor/cakephp/cakephp/src/Database/Dialect/SqliteDialectTrait.php new file mode 100644 index 000000000..24550909c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Dialect/SqliteDialectTrait.php @@ -0,0 +1,196 @@ + 'd', + 'hour' => 'H', + 'month' => 'm', + 'minute' => 'M', + 'second' => 'S', + 'week' => 'W', + 'year' => 'Y' + ]; + + /** + * Returns a dictionary of expressions to be transformed when compiling a Query + * to SQL. Array keys are method names to be called in this class + * + * @return array + */ + protected function _expressionTranslators() + { + $namespace = 'Cake\Database\Expression'; + + return [ + $namespace . '\FunctionExpression' => '_transformFunctionExpression', + $namespace . '\TupleComparison' => '_transformTupleComparison' + ]; + } + + /** + * Receives a FunctionExpression and changes it so that it conforms to this + * SQL dialect. + * + * @param \Cake\Database\Expression\FunctionExpression $expression The function expression + * to translate for SQLite. + * @return void + */ + protected function _transformFunctionExpression(FunctionExpression $expression) + { + switch ($expression->getName()) { + case 'CONCAT': + // CONCAT function is expressed as exp1 || exp2 + $expression->setName('')->setConjunction(' ||'); + break; + case 'DATEDIFF': + $expression + ->setName('ROUND') + ->setConjunction('-') + ->iterateParts(function ($p) { + return new FunctionExpression('JULIANDAY', [$p['value']], [$p['type']]); + }); + break; + case 'NOW': + $expression->setName('DATETIME')->add(["'now'" => 'literal']); + break; + case 'CURRENT_DATE': + $expression->setName('DATE')->add(["'now'" => 'literal']); + break; + case 'CURRENT_TIME': + $expression->setName('TIME')->add(["'now'" => 'literal']); + break; + case 'EXTRACT': + $expression + ->setName('STRFTIME') + ->setConjunction(' ,') + ->iterateParts(function ($p, $key) { + if ($key === 0) { + $value = rtrim(strtolower($p), 's'); + if (isset($this->_dateParts[$value])) { + $p = ['value' => '%' . $this->_dateParts[$value], 'type' => null]; + } + } + + return $p; + }); + break; + case 'DATE_ADD': + $expression + ->setName('DATE') + ->setConjunction(',') + ->iterateParts(function ($p, $key) { + if ($key === 1) { + $p = ['value' => $p, 'type' => null]; + } + + return $p; + }); + break; + case 'DAYOFWEEK': + $expression + ->setName('STRFTIME') + ->setConjunction(' ') + ->add(["'%w', " => 'literal'], [], true) + ->add([') + (1' => 'literal']); // Sqlite starts on index 0 but Sunday should be 1 + break; + } + } + + /** + * Get the schema dialect. + * + * Used by Cake\Database\Schema package to reflect schema and + * generate schema. + * + * @return \Cake\Database\Schema\SqliteSchema + */ + public function schemaDialect() + { + if (!$this->_schemaDialect) { + $this->_schemaDialect = new SqliteSchema($this); + } + + return $this->_schemaDialect; + } + + /** + * {@inheritDoc} + */ + public function disableForeignKeySQL() + { + return 'PRAGMA foreign_keys = OFF'; + } + + /** + * {@inheritDoc} + */ + public function enableForeignKeySQL() + { + return 'PRAGMA foreign_keys = ON'; + } + + /** + * {@inheritDoc} + * + * @return \Cake\Database\SqliteCompiler + */ + public function newCompiler() + { + return new SqliteCompiler(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Dialect/SqlserverDialectTrait.php b/app/vendor/cakephp/cakephp/src/Database/Dialect/SqlserverDialectTrait.php new file mode 100644 index 000000000..a58161c08 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Dialect/SqlserverDialectTrait.php @@ -0,0 +1,393 @@ +clause('limit'); + $offset = $query->clause('offset'); + + if ($limit && $offset === null) { + $query->modifier(['_auto_top_' => sprintf('TOP %d', $limit)]); + } + + if ($offset !== null && !$query->clause('order')) { + $query->order($query->newExpr()->add('(SELECT NULL)')); + } + + if ($this->_version() < 11 && $offset !== null) { + return $this->_pagingSubquery($query, $limit, $offset); + } + + return $this->_transformDistinct($query); + } + + /** + * Get the version of SQLserver we are connected to. + * + * @return int + */ + // @codingStandardsIgnoreLine + public function _version() + { + $this->connect(); + + return $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + /** + * Generate a paging subquery for older versions of SQLserver. + * + * Prior to SQLServer 2012 there was no equivalent to LIMIT OFFSET, so a subquery must + * be used. + * + * @param \Cake\Database\Query $original The query to wrap in a subquery. + * @param int $limit The number of rows to fetch. + * @param int $offset The number of rows to offset. + * @return \Cake\Database\Query Modified query object. + */ + protected function _pagingSubquery($original, $limit, $offset) + { + $field = '_cake_paging_._cake_page_rownum_'; + + if ($original->clause('order')) { + // SQL server does not support column aliases in OVER clauses. But + // the only practical way to specify the use of calculated columns + // is with their alias. So substitute the select SQL in place of + // any column aliases for those entries in the order clause. + $select = $original->clause('select'); + $order = new OrderByExpression(); + $original + ->clause('order') + ->iterateParts(function ($direction, $orderBy) use ($select, $order) { + $key = $orderBy; + if (isset($select[$orderBy]) && + $select[$orderBy] instanceof ExpressionInterface + ) { + $key = $select[$orderBy]->sql(new ValueBinder()); + } + $order->add([$key => $direction]); + + // Leave original order clause unchanged. + return $orderBy; + }); + } else { + $order = new OrderByExpression('(SELECT NULL)'); + } + + $query = clone $original; + $query->select([ + '_cake_page_rownum_' => new UnaryExpression('ROW_NUMBER() OVER', $order) + ])->limit(null) + ->offset(null) + ->order([], true); + + $outer = new Query($query->getConnection()); + $outer->select('*') + ->from(['_cake_paging_' => $query]); + + if ($offset) { + $outer->where(["$field > " . (int)$offset]); + } + if ($limit) { + $value = (int)$offset + (int)$limit; + $outer->where(["$field <= $value"]); + } + + // Decorate the original query as that is what the + // end developer will be calling execute() on originally. + $original->decorateResults(function ($row) { + if (isset($row['_cake_page_rownum_'])) { + unset($row['_cake_page_rownum_']); + } + + return $row; + }); + + return $outer; + } + + /** + * Returns the passed query after rewriting the DISTINCT clause, so that drivers + * that do not support the "ON" part can provide the actual way it should be done + * + * @param \Cake\Database\Query $original The query to be transformed + * @return \Cake\Database\Query + */ + protected function _transformDistinct($original) + { + if (!is_array($original->clause('distinct'))) { + return $original; + } + + $query = clone $original; + $distinct = $query->clause('distinct'); + $query->distinct(false); + + $order = new OrderByExpression($distinct); + $query + ->select(function ($q) use ($distinct, $order) { + $over = $q->newExpr('ROW_NUMBER() OVER') + ->add('(PARTITION BY') + ->add($q->newExpr()->add($distinct)->setConjunction(',')) + ->add($order) + ->add(')') + ->setConjunction(' '); + + return [ + '_cake_distinct_pivot_' => $over + ]; + }) + ->limit(null) + ->offset(null) + ->order([], true); + + $outer = new Query($query->getConnection()); + $outer->select('*') + ->from(['_cake_distinct_' => $query]) + ->where(['_cake_distinct_pivot_' => 1]); + + // Decorate the original query as that is what the + // end developer will be calling execute() on originally. + $original->decorateResults(function ($row) { + if (isset($row['_cake_distinct_pivot_'])) { + unset($row['_cake_distinct_pivot_']); + } + + return $row; + }); + + return $outer; + } + + /** + * Returns a dictionary of expressions to be transformed when compiling a Query + * to SQL. Array keys are method names to be called in this class + * + * @return array + */ + protected function _expressionTranslators() + { + $namespace = 'Cake\Database\Expression'; + + return [ + $namespace . '\FunctionExpression' => '_transformFunctionExpression', + $namespace . '\TupleComparison' => '_transformTupleComparison' + ]; + } + + /** + * Receives a FunctionExpression and changes it so that it conforms to this + * SQL dialect. + * + * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL. + * @return void + */ + protected function _transformFunctionExpression(FunctionExpression $expression) + { + switch ($expression->getName()) { + case 'CONCAT': + // CONCAT function is expressed as exp1 + exp2 + $expression->setName('')->setConjunction(' +'); + break; + case 'DATEDIFF': + $hasDay = false; + $visitor = function ($value) use (&$hasDay) { + if ($value === 'day') { + $hasDay = true; + } + + return $value; + }; + $expression->iterateParts($visitor); + + if (!$hasDay) { + $expression->add(['day' => 'literal'], [], true); + } + break; + case 'CURRENT_DATE': + $time = new FunctionExpression('GETUTCDATE'); + $expression->setName('CONVERT')->add(['date' => 'literal', $time]); + break; + case 'CURRENT_TIME': + $time = new FunctionExpression('GETUTCDATE'); + $expression->setName('CONVERT')->add(['time' => 'literal', $time]); + break; + case 'NOW': + $expression->setName('GETUTCDATE'); + break; + case 'EXTRACT': + $expression->setName('DATEPART')->setConjunction(' ,'); + break; + case 'DATE_ADD': + $params = []; + $visitor = function ($p, $key) use (&$params) { + if ($key === 0) { + $params[2] = $p; + } else { + $valueUnit = explode(' ', $p); + $params[0] = rtrim($valueUnit[1], 's'); + $params[1] = $valueUnit[0]; + } + + return $p; + }; + $manipulator = function ($p, $key) use (&$params) { + return $params[$key]; + }; + + $expression + ->setName('DATEADD') + ->setConjunction(',') + ->iterateParts($visitor) + ->iterateParts($manipulator) + ->add([$params[2] => 'literal']); + break; + case 'DAYOFWEEK': + $expression + ->setName('DATEPART') + ->setConjunction(' ') + ->add(['weekday, ' => 'literal'], [], true); + break; + case 'SUBSTR': + $expression->setName('SUBSTRING'); + if (count($expression) < 4) { + $params = []; + $expression + ->iterateParts(function ($p) use (&$params) { + return $params[] = $p; + }) + ->add([new FunctionExpression('LEN', [$params[0]]), ['string']]); + } + + break; + } + } + + /** + * Get the schema dialect. + * + * Used by Cake\Schema package to reflect schema and + * generate schema. + * + * @return \Cake\Database\Schema\SqlserverSchema + */ + public function schemaDialect() + { + return new SqlserverSchema($this); + } + + /** + * Returns a SQL snippet for creating a new transaction savepoint + * + * @param string $name save point name + * @return string + */ + public function savePointSQL($name) + { + return 'SAVE TRANSACTION t' . $name; + } + + /** + * Returns a SQL snippet for releasing a previously created save point + * + * @param string $name save point name + * @return string + */ + public function releaseSavePointSQL($name) + { + return 'COMMIT TRANSACTION t' . $name; + } + + /** + * Returns a SQL snippet for rollbacking a previously created save point + * + * @param string $name save point name + * @return string + */ + public function rollbackSavePointSQL($name) + { + return 'ROLLBACK TRANSACTION t' . $name; + } + + /** + * {@inheritDoc} + * + * @return \Cake\Database\SqlserverCompiler + */ + public function newCompiler() + { + return new SqlserverCompiler(); + } + + /** + * {@inheritDoc} + */ + public function disableForeignKeySQL() + { + return 'EXEC sp_msforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"'; + } + + /** + * {@inheritDoc} + */ + public function enableForeignKeySQL() + { + return 'EXEC sp_msforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all"'; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Dialect/TupleComparisonTranslatorTrait.php b/app/vendor/cakephp/cakephp/src/Database/Dialect/TupleComparisonTranslatorTrait.php new file mode 100644 index 000000000..a9627b3b6 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Dialect/TupleComparisonTranslatorTrait.php @@ -0,0 +1,95 @@ +getField(); + + if (!is_array($fields)) { + return; + } + + $value = $expression->getValue(); + $op = $expression->getOperator(); + $true = new QueryExpression('1'); + + if ($value instanceof Query) { + $selected = array_values($value->clause('select')); + foreach ($fields as $i => $field) { + $value->andWhere([$field . " $op" => new IdentifierExpression($selected[$i])]); + } + $value->select($true, true); + $expression->setField($true); + $expression->setOperator('='); + + return; + } + + $surrogate = $query->getConnection() + ->newQuery() + ->select($true); + + if (!is_array(current($value))) { + $value = [$value]; + } + + $conditions = ['OR' => []]; + foreach ($value as $tuple) { + $item = []; + foreach (array_values($tuple) as $i => $value) { + $item[] = [$fields[$i] => $value]; + } + $conditions['OR'][] = $item; + } + $surrogate->where($conditions); + + $expression->setField($true); + $expression->setValue($surrogate); + $expression->setOperator('='); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver.php b/app/vendor/cakephp/cakephp/src/Database/Driver.php new file mode 100644 index 000000000..3cc0bb613 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Driver.php @@ -0,0 +1,437 @@ +_baseConfig; + $this->_config = $config; + if (!empty($config['quoteIdentifiers'])) { + $this->enableAutoQuoting(); + } + } + + /** + * Establishes a connection to the database server + * + * @param string $dsn A Driver-specific PDO-DSN + * @param array $config configuration to be used for creating connection + * @return bool true on success + */ + protected function _connect($dsn, array $config) + { + $connection = new PDO( + $dsn, + $config['username'], + $config['password'], + $config['flags'] + ); + $this->setConnection($connection); + + return true; + } + + /** + * {@inheritDoc} + */ + abstract public function connect(); + + /** + * {@inheritDoc} + */ + public function disconnect() + { + $this->_connection = null; + } + + /** + * Returns correct connection resource or object that is internally used + * If first argument is passed, it will set internal connection object or + * result to the value passed. + * + * @param mixed $connection The PDO connection instance. + * @return mixed Connection object used internally. + * @deprecated 3.6.0 Use getConnection()/setConnection() instead. + */ + public function connection($connection = null) + { + deprecationWarning( + get_called_class() . '::connection() is deprecated. ' . + 'Use setConnection()/getConnection() instead.' + ); + if ($connection !== null) { + $this->_connection = $connection; + } + + return $this->_connection; + } + + /** + * Get the internal PDO connection instance. + * + * @return \PDO + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * Set the internal PDO connection instance. + * + * @param \PDO $connection PDO instance. + * @return $this + */ + public function setConnection($connection) + { + $this->_connection = $connection; + + return $this; + } + + /** + * {@inheritDoc} + */ + abstract public function enabled(); + + /** + * {@inheritDoc} + */ + public function prepare($query) + { + $this->connect(); + $isObject = $query instanceof Query; + $statement = $this->_connection->prepare($isObject ? $query->sql() : $query); + + return new PDOStatement($statement, $this); + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + $this->connect(); + if ($this->_connection->inTransaction()) { + return true; + } + + return $this->_connection->beginTransaction(); + } + + /** + * {@inheritDoc} + */ + public function commitTransaction() + { + $this->connect(); + if (!$this->_connection->inTransaction()) { + return false; + } + + return $this->_connection->commit(); + } + + /** + * {@inheritDoc} + */ + public function rollbackTransaction() + { + $this->connect(); + if (!$this->_connection->inTransaction()) { + return false; + } + + return $this->_connection->rollBack(); + } + + /** + * {@inheritDoc} + */ + abstract public function releaseSavePointSQL($name); + + /** + * {@inheritDoc} + */ + abstract public function savePointSQL($name); + + /** + * {@inheritDoc} + */ + abstract public function rollbackSavePointSQL($name); + + /** + * {@inheritDoc} + */ + abstract public function disableForeignKeySQL(); + + /** + * {@inheritDoc} + */ + abstract public function enableForeignKeySQL(); + + /** + * {@inheritDoc} + */ + abstract public function supportsDynamicConstraints(); + + /** + * {@inheritDoc} + */ + public function supportsSavePoints() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function quote($value, $type) + { + $this->connect(); + + return $this->_connection->quote($value, $type); + } + + /** + * Checks if the driver supports quoting, as PDO_ODBC does not support it. + * + * @return bool + */ + public function supportsQuoting() + { + $this->connect(); + + return $this->_connection->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'odbc'; + } + + /** + * {@inheritDoc} + */ + abstract public function queryTranslator($type); + + /** + * {@inheritDoc} + */ + abstract public function schemaDialect(); + + /** + * {@inheritDoc} + */ + abstract public function quoteIdentifier($identifier); + + /** + * {@inheritDoc} + */ + public function schemaValue($value) + { + if ($value === null) { + return 'NULL'; + } + if ($value === false) { + return 'FALSE'; + } + if ($value === true) { + return 'TRUE'; + } + if (is_float($value)) { + return str_replace(',', '.', (string)$value); + } + if ((is_int($value) || $value === '0') || ( + is_numeric($value) && strpos($value, ',') === false && + $value[0] !== '0' && strpos($value, 'e') === false) + ) { + return (string)$value; + } + + return $this->_connection->quote($value, PDO::PARAM_STR); + } + + /** + * {@inheritDoc} + */ + public function schema() + { + return $this->_config['schema']; + } + + /** + * {@inheritDoc} + */ + public function lastInsertId($table = null, $column = null) + { + $this->connect(); + + if ($this->_connection instanceof PDO) { + return $this->_connection->lastInsertId($table); + } + + return $this->_connection->lastInsertId($table, $column); + } + + /** + * {@inheritDoc} + */ + public function isConnected() + { + if ($this->_connection === null) { + $connected = false; + } else { + try { + $connected = $this->_connection->query('SELECT 1'); + } catch (PDOException $e) { + $connected = false; + } + } + + return (bool)$connected; + } + + /** + * {@inheritDoc} + */ + public function enableAutoQuoting($enable = true) + { + $this->_autoQuoting = (bool)$enable; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isAutoQuotingEnabled() + { + return $this->_autoQuoting; + } + + /** + * Returns whether or not this driver should automatically quote identifiers + * in queries + * + * If called with a boolean argument, it will toggle the auto quoting setting + * to the passed value + * + * @deprecated 3.4.0 use enableAutoQuoting()/isAutoQuotingEnabled() instead. + * @param bool|null $enable Whether to enable auto quoting + * @return bool + */ + public function autoQuoting($enable = null) + { + deprecationWarning( + 'Driver::autoQuoting() is deprecated. ' . + 'Use Driver::enableAutoQuoting()/isAutoQuotingEnabled() instead.' + ); + if ($enable !== null) { + $this->enableAutoQuoting($enable); + } + + return $this->isAutoQuotingEnabled(); + } + + /** + * {@inheritDoc} + */ + public function compileQuery(Query $query, ValueBinder $generator) + { + $processor = $this->newCompiler(); + $translator = $this->queryTranslator($query->type()); + $query = $translator($query); + + return [$query, $processor->compile($query, $generator)]; + } + + /** + * {@inheritDoc} + */ + public function newCompiler() + { + return new QueryCompiler(); + } + + /** + * Destructor + */ + public function __destruct() + { + $this->_connection = null; + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + return [ + 'connected' => $this->_connection !== null + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver/Mysql.php b/app/vendor/cakephp/cakephp/src/Database/Driver/Mysql.php new file mode 100644 index 000000000..7a9f11ee3 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Driver/Mysql.php @@ -0,0 +1,180 @@ + true, + 'host' => 'localhost', + 'username' => 'root', + 'password' => '', + 'database' => 'cake', + 'port' => '3306', + 'flags' => [], + 'encoding' => 'utf8mb4', + 'timezone' => null, + 'init' => [], + ]; + + /** + * The server version + * + * @var string + */ + protected $_version; + + /** + * Whether or not the server supports native JSON + * + * @var bool + */ + protected $_supportsNativeJson; + + /** + * Establishes a connection to the database server + * + * @return bool true on success + */ + public function connect() + { + if ($this->_connection) { + return true; + } + $config = $this->_config; + + if ($config['timezone'] === 'UTC') { + $config['timezone'] = '+0:00'; + } + + if (!empty($config['timezone'])) { + $config['init'][] = sprintf("SET time_zone = '%s'", $config['timezone']); + } + if (!empty($config['encoding'])) { + $config['init'][] = sprintf('SET NAMES %s', $config['encoding']); + } + + $config['flags'] += [ + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ]; + + if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) { + $config['flags'][PDO::MYSQL_ATTR_SSL_KEY] = $config['ssl_key']; + $config['flags'][PDO::MYSQL_ATTR_SSL_CERT] = $config['ssl_cert']; + } + if (!empty($config['ssl_ca'])) { + $config['flags'][PDO::MYSQL_ATTR_SSL_CA] = $config['ssl_ca']; + } + + if (empty($config['unix_socket'])) { + $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']};charset={$config['encoding']}"; + } else { + $dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}"; + } + + $this->_connect($dsn, $config); + + if (!empty($config['init'])) { + $connection = $this->getConnection(); + foreach ((array)$config['init'] as $command) { + $connection->exec($command); + } + } + + return true; + } + + /** + * Returns whether php is able to use this driver for connecting to database + * + * @return bool true if it is valid to use this driver + */ + public function enabled() + { + return in_array('mysql', PDO::getAvailableDrivers()); + } + + /** + * Prepares a sql statement to be executed + * + * @param string|\Cake\Database\Query $query The query to prepare. + * @return \Cake\Database\StatementInterface + */ + public function prepare($query) + { + $this->connect(); + $isObject = $query instanceof Query; + $statement = $this->_connection->prepare($isObject ? $query->sql() : $query); + $result = new MysqlStatement($statement, $this); + if ($isObject && $query->isBufferedResultsEnabled() === false) { + $result->bufferResults(false); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function schema() + { + return $this->_config['database']; + } + + /** + * {@inheritDoc} + */ + public function supportsDynamicConstraints() + { + return true; + } + + /** + * Returns true if the server supports native JSON columns + * + * @return bool + */ + public function supportsNativeJson() + { + if ($this->_supportsNativeJson !== null) { + return $this->_supportsNativeJson; + } + + if ($this->_version === null) { + $this->_version = $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + return $this->_supportsNativeJson = version_compare($this->_version, '5.7.0', '>='); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver/PDODriverTrait.php b/app/vendor/cakephp/cakephp/src/Database/Driver/PDODriverTrait.php new file mode 100644 index 000000000..388bd70f2 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Driver/PDODriverTrait.php @@ -0,0 +1,203 @@ +connection($connection); + + return true; + } + + /** + * Returns correct connection resource or object that is internally used + * If first argument is passed, it will set internal connection object or + * result to the value passed + * + * @param null|\PDO $connection The PDO connection instance. + * @return \PDO connection object used internally + */ + public function connection($connection = null) + { + if ($connection !== null) { + $this->_connection = $connection; + } + + return $this->_connection; + } + + /** + * Disconnects from database server + * + * @return void + */ + public function disconnect() + { + $this->_connection = null; + } + + /** + * Checks whether or not the driver is connected. + * + * @return bool + */ + public function isConnected() + { + if ($this->_connection === null) { + $connected = false; + } else { + try { + $connected = $this->_connection->query('SELECT 1'); + } catch (PDOException $e) { + $connected = false; + } + } + + return (bool)$connected; + } + + /** + * Prepares a sql statement to be executed + * + * @param string|\Cake\Database\Query $query The query to turn into a prepared statement. + * @return \Cake\Database\StatementInterface + */ + public function prepare($query) + { + $this->connect(); + $isObject = $query instanceof Query; + $statement = $this->_connection->prepare($isObject ? $query->sql() : $query); + + return new PDOStatement($statement, $this); + } + + /** + * Starts a transaction + * + * @return bool true on success, false otherwise + */ + public function beginTransaction() + { + $this->connect(); + if ($this->_connection->inTransaction()) { + return true; + } + + return $this->_connection->beginTransaction(); + } + + /** + * Commits a transaction + * + * @return bool true on success, false otherwise + */ + public function commitTransaction() + { + $this->connect(); + if (!$this->_connection->inTransaction()) { + return false; + } + + return $this->_connection->commit(); + } + + /** + * Rollback a transaction + * + * @return bool true on success, false otherwise + */ + public function rollbackTransaction() + { + $this->connect(); + if (!$this->_connection->inTransaction()) { + return false; + } + + return $this->_connection->rollBack(); + } + + /** + * Returns a value in a safe representation to be used in a query string + * + * @param mixed $value The value to quote. + * @param string $type Type to be used for determining kind of quoting to perform + * @return string + */ + public function quote($value, $type) + { + $this->connect(); + + return $this->_connection->quote($value, $type); + } + + /** + * Returns last id generated for a table or sequence in database + * + * @param string|null $table table name or sequence to get last insert value from + * @param string|null $column the name of the column representing the primary key + * @return string|int + */ + public function lastInsertId($table = null, $column = null) + { + $this->connect(); + + return $this->_connection->lastInsertId($table); + } + + /** + * Checks if the driver supports quoting, as PDO_ODBC does not support it. + * + * @return bool + */ + public function supportsQuoting() + { + $this->connect(); + + return $this->_connection->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'odbc'; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver/Postgres.php b/app/vendor/cakephp/cakephp/src/Database/Driver/Postgres.php new file mode 100644 index 000000000..2a2fae8fa --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Driver/Postgres.php @@ -0,0 +1,133 @@ + true, + 'host' => 'localhost', + 'username' => 'root', + 'password' => '', + 'database' => 'cake', + 'schema' => 'public', + 'port' => 5432, + 'encoding' => 'utf8', + 'timezone' => null, + 'flags' => [], + 'init' => [], + ]; + + /** + * Establishes a connection to the database server + * + * @return bool true on success + */ + public function connect() + { + if ($this->_connection) { + return true; + } + $config = $this->_config; + $config['flags'] += [ + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]; + if (empty($config['unix_socket'])) { + $dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}"; + } else { + $dsn = "pgsql:dbname={$config['database']}"; + } + + $this->_connect($dsn, $config); + $this->_connection = $connection = $this->getConnection(); + if (!empty($config['encoding'])) { + $this->setEncoding($config['encoding']); + } + + if (!empty($config['schema'])) { + $this->setSchema($config['schema']); + } + + if (!empty($config['timezone'])) { + $config['init'][] = sprintf('SET timezone = %s', $connection->quote($config['timezone'])); + } + + foreach ($config['init'] as $command) { + $connection->exec($command); + } + + return true; + } + + /** + * Returns whether php is able to use this driver for connecting to database + * + * @return bool true if it is valid to use this driver + */ + public function enabled() + { + return in_array('pgsql', PDO::getAvailableDrivers()); + } + + /** + * Sets connection encoding + * + * @param string $encoding The encoding to use. + * @return void + */ + public function setEncoding($encoding) + { + $this->connect(); + $this->_connection->exec('SET NAMES ' . $this->_connection->quote($encoding)); + } + + /** + * Sets connection default schema, if any relation defined in a query is not fully qualified + * postgres will fallback to looking the relation into defined default schema + * + * @param string $schema The schema names to set `search_path` to. + * @return void + */ + public function setSchema($schema) + { + $this->connect(); + $this->_connection->exec('SET search_path TO ' . $this->_connection->quote($schema)); + } + + /** + * {@inheritDoc} + */ + public function supportsDynamicConstraints() + { + return true; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlite.php b/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlite.php new file mode 100644 index 000000000..9b518e687 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlite.php @@ -0,0 +1,123 @@ + false, + 'username' => null, + 'password' => null, + 'database' => ':memory:', + 'encoding' => 'utf8', + 'mask' => 0644, + 'flags' => [], + 'init' => [], + ]; + + /** + * Establishes a connection to the database server + * + * @return bool true on success + */ + public function connect() + { + if ($this->_connection) { + return true; + } + $config = $this->_config; + $config['flags'] += [ + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]; + + $databaseExists = file_exists($config['database']); + + $dsn = "sqlite:{$config['database']}"; + $this->_connect($dsn, $config); + + if (!$databaseExists && $config['database'] != ':memory:') { + //@codingStandardsIgnoreStart + @chmod($config['database'], $config['mask']); + //@codingStandardsIgnoreEnd + } + + if (!empty($config['init'])) { + foreach ((array)$config['init'] as $command) { + $this->getConnection()->exec($command); + } + } + + return true; + } + + /** + * Returns whether php is able to use this driver for connecting to database + * + * @return bool true if it is valid to use this driver + */ + public function enabled() + { + return in_array('sqlite', PDO::getAvailableDrivers()); + } + + /** + * Prepares a sql statement to be executed + * + * @param string|\Cake\Database\Query $query The query to prepare. + * @return \Cake\Database\StatementInterface + */ + public function prepare($query) + { + $this->connect(); + $isObject = $query instanceof Query; + $statement = $this->_connection->prepare($isObject ? $query->sql() : $query); + $result = new SqliteStatement(new PDOStatement($statement, $this), $this); + if ($isObject && $query->isBufferedResultsEnabled() === false) { + $result->bufferResults(false); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function supportsDynamicConstraints() + { + return false; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlserver.php b/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlserver.php new file mode 100644 index 000000000..74d72fc5e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlserver.php @@ -0,0 +1,164 @@ + 'localhost\SQLEXPRESS', + 'username' => '', + 'password' => '', + 'database' => 'cake', + 'port' => '', + // PDO::SQLSRV_ENCODING_UTF8 + 'encoding' => 65001, + 'flags' => [], + 'init' => [], + 'settings' => [], + 'attributes' => [], + 'app' => null, + 'connectionPooling' => null, + 'failoverPartner' => null, + 'loginTimeout' => null, + 'multiSubnetFailover' => null, + ]; + + /** + * Establishes a connection to the database server. + * + * Please note that the PDO::ATTR_PERSISTENT attribute is not supported by + * the SQL Server PHP PDO drivers. As a result you cannot use the + * persistent config option when connecting to a SQL Server (for more + * information see: https://github.com/Microsoft/msphpsql/issues/65). + * + * @throws \InvalidArgumentException if an unsupported setting is in the driver config + * @return bool true on success + */ + public function connect() + { + if ($this->_connection) { + return true; + } + $config = $this->_config; + + if (isset($config['persistent']) && $config['persistent']) { + throw new \InvalidArgumentException('Config setting "persistent" cannot be set to true, as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT'); + } + + $config['flags'] += [ + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]; + + if (!empty($config['encoding'])) { + $config['flags'][PDO::SQLSRV_ATTR_ENCODING] = $config['encoding']; + } + $port = ''; + if (strlen($config['port'])) { + $port = ',' . $config['port']; + } + + $dsn = "sqlsrv:Server={$config['host']}{$port};Database={$config['database']};MultipleActiveResultSets=false"; + if ($config['app'] !== null) { + $dsn .= ";APP={$config['app']}"; + } + if ($config['connectionPooling'] !== null) { + $dsn .= ";ConnectionPooling={$config['connectionPooling']}"; + } + if ($config['failoverPartner'] !== null) { + $dsn .= ";Failover_Partner={$config['failoverPartner']}"; + } + if ($config['loginTimeout'] !== null) { + $dsn .= ";LoginTimeout={$config['loginTimeout']}"; + } + if ($config['multiSubnetFailover'] !== null) { + $dsn .= ";MultiSubnetFailover={$config['multiSubnetFailover']}"; + } + $this->_connect($dsn, $config); + + $connection = $this->getConnection(); + if (!empty($config['init'])) { + foreach ((array)$config['init'] as $command) { + $connection->exec($command); + } + } + if (!empty($config['settings']) && is_array($config['settings'])) { + foreach ($config['settings'] as $key => $value) { + $connection->exec("SET {$key} {$value}"); + } + } + if (!empty($config['attributes']) && is_array($config['attributes'])) { + foreach ($config['attributes'] as $key => $value) { + $connection->setAttribute($key, $value); + } + } + + return true; + } + + /** + * Returns whether PHP is able to use this driver for connecting to database + * + * @return bool true if it is valid to use this driver + */ + public function enabled() + { + return in_array('sqlsrv', PDO::getAvailableDrivers()); + } + + /** + * Prepares a sql statement to be executed + * + * @param string|\Cake\Database\Query $query The query to prepare. + * @return \Cake\Database\StatementInterface + */ + public function prepare($query) + { + $this->connect(); + $options = [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]; + $isObject = $query instanceof Query; + if ($isObject && $query->isBufferedResultsEnabled() === false) { + $options = []; + } + $statement = $this->_connection->prepare($isObject ? $query->sql() : $query, $options); + + return new SqlserverStatement($statement, $this); + } + + /** + * {@inheritDoc} + */ + public function supportsDynamicConstraints() + { + return true; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/DriverInterface.php b/app/vendor/cakephp/cakephp/src/Database/DriverInterface.php new file mode 100644 index 000000000..3ac4b64cb --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/DriverInterface.php @@ -0,0 +1,256 @@ +_castToExpression($from, $type); + $to = $this->_castToExpression($to, $type); + } + + $this->_field = $field; + $this->_from = $from; + $this->_to = $to; + $this->_type = $type; + } + + /** + * Converts the expression to its string representation + * + * @param \Cake\Database\ValueBinder $generator Placeholder generator object + * @return string + */ + public function sql(ValueBinder $generator) + { + $parts = [ + 'from' => $this->_from, + 'to' => $this->_to + ]; + + $field = $this->_field; + if ($field instanceof ExpressionInterface) { + $field = $field->sql($generator); + } + + foreach ($parts as $name => $part) { + if ($part instanceof ExpressionInterface) { + $parts[$name] = $part->sql($generator); + continue; + } + $parts[$name] = $this->_bindValue($part, $generator, $this->_type); + } + + return sprintf('%s BETWEEN %s AND %s', $field, $parts['from'], $parts['to']); + } + + /** + * {@inheritDoc} + * + */ + public function traverse(callable $callable) + { + foreach ([$this->_field, $this->_from, $this->_to] as $part) { + if ($part instanceof ExpressionInterface) { + $callable($part); + } + } + } + + /** + * Registers a value in the placeholder generator and returns the generated placeholder + * + * @param mixed $value The value to bind + * @param \Cake\Database\ValueBinder $generator The value binder to use + * @param string $type The type of $value + * @return string generated placeholder + */ + protected function _bindValue($value, $generator, $type) + { + $placeholder = $generator->placeholder('c'); + $generator->bind($placeholder, $value, $type); + + return $placeholder; + } + + /** + * Do a deep clone of this expression. + * + * @return void + */ + public function __clone() + { + foreach (['_field', '_from', '_to'] as $part) { + if ($this->{$part} instanceof ExpressionInterface) { + $this->{$part} = clone $this->{$part}; + } + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/CaseExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/CaseExpression.php new file mode 100644 index 000000000..b826c183e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Expression/CaseExpression.php @@ -0,0 +1,249 @@ + :value" + * + * @var array + */ + protected $_conditions = []; + + /** + * Values that are associated with the conditions in the $_conditions array. + * Each value represents the 'true' value for the condition with the corresponding key. + * + * @var array + */ + protected $_values = []; + + /** + * The `ELSE` value for the case statement. If null then no `ELSE` will be included. + * + * @var string|\Cake\Database\ExpressionInterface|array|null + */ + protected $_elseValue; + + /** + * Constructs the case expression + * + * @param array|\Cake\Database\ExpressionInterface $conditions The conditions to test. Must be a ExpressionInterface + * instance, or an array of ExpressionInterface instances. + * @param array|\Cake\Database\ExpressionInterface $values associative array of values to be associated with the conditions + * passed in $conditions. If there are more $values than $conditions, the last $value is used as the `ELSE` value + * @param array $types associative array of types to be associated with the values + * passed in $values + */ + public function __construct($conditions = [], $values = [], $types = []) + { + if (!empty($conditions)) { + $this->add($conditions, $values, $types); + } + + if (is_array($conditions) && is_array($values) && count($values) > count($conditions)) { + end($values); + $key = key($values); + $this->elseValue($values[$key], isset($types[$key]) ? $types[$key] : null); + } + } + + /** + * Adds one or more conditions and their respective true values to the case object. + * Conditions must be a one dimensional array or a QueryExpression. + * The trueValues must be a similar structure, but may contain a string value. + * + * @param array|\Cake\Database\ExpressionInterface $conditions Must be a ExpressionInterface instance, or an array of ExpressionInterface instances. + * @param array|\Cake\Database\ExpressionInterface $values associative array of values of each condition + * @param array $types associative array of types to be associated with the values + * + * @return $this + */ + public function add($conditions = [], $values = [], $types = []) + { + if (!is_array($conditions)) { + $conditions = [$conditions]; + } + if (!is_array($values)) { + $values = [$values]; + } + if (!is_array($types)) { + $types = [$types]; + } + + $this->_addExpressions($conditions, $values, $types); + + return $this; + } + + /** + * Iterates over the passed in conditions and ensures that there is a matching true value for each. + * If no matching true value, then it is defaulted to '1'. + * + * @param array|\Cake\Database\ExpressionInterface $conditions Must be a ExpressionInterface instance, or an array of ExpressionInterface instances. + * @param array|\Cake\Database\ExpressionInterface $values associative array of values of each condition + * @param array $types associative array of types to be associated with the values + * + * @return void + */ + protected function _addExpressions($conditions, $values, $types) + { + $rawValues = array_values($values); + $keyValues = array_keys($values); + + foreach ($conditions as $k => $c) { + $numericKey = is_numeric($k); + + if ($numericKey && empty($c)) { + continue; + } + + if (!$c instanceof ExpressionInterface) { + continue; + } + + $this->_conditions[] = $c; + $value = isset($rawValues[$k]) ? $rawValues[$k] : 1; + + if ($value === 'literal') { + $value = $keyValues[$k]; + $this->_values[] = $value; + continue; + } + + if ($value === 'identifier') { + $value = new IdentifierExpression($keyValues[$k]); + $this->_values[] = $value; + continue; + } + + $type = isset($types[$k]) ? $types[$k] : null; + + if ($type !== null && !$value instanceof ExpressionInterface) { + $value = $this->_castToExpression($value, $type); + } + + if ($value instanceof ExpressionInterface) { + $this->_values[] = $value; + continue; + } + + $this->_values[] = ['value' => $value, 'type' => $type]; + } + } + + /** + * Sets the default value + * + * @param \Cake\Database\ExpressionInterface|string|array|null $value Value to set + * @param string|null $type Type of value + * + * @return void + */ + public function elseValue($value = null, $type = null) + { + if (is_array($value)) { + end($value); + $value = key($value); + } + + if ($value !== null && !$value instanceof ExpressionInterface) { + $value = $this->_castToExpression($value, $type); + } + + if (!$value instanceof ExpressionInterface) { + $value = ['value' => $value, 'type' => $type]; + } + + $this->_elseValue = $value; + } + + /** + * Compiles the relevant parts into sql + * + * @param array|string|\Cake\Database\ExpressionInterface $part The part to compile + * @param \Cake\Database\ValueBinder $generator Sql generator + * + * @return string + */ + protected function _compile($part, ValueBinder $generator) + { + if ($part instanceof ExpressionInterface) { + $part = $part->sql($generator); + } elseif (is_array($part)) { + $placeholder = $generator->placeholder('param'); + $generator->bind($placeholder, $part['value'], $part['type']); + $part = $placeholder; + } + + return $part; + } + + /** + * Converts the Node into a SQL string fragment. + * + * @param \Cake\Database\ValueBinder $generator Placeholder generator object + * + * @return string + */ + public function sql(ValueBinder $generator) + { + $parts = []; + $parts[] = 'CASE'; + foreach ($this->_conditions as $k => $part) { + $value = $this->_values[$k]; + $parts[] = 'WHEN ' . $this->_compile($part, $generator) . ' THEN ' . $this->_compile($value, $generator); + } + if ($this->_elseValue !== null) { + $parts[] = 'ELSE'; + $parts[] = $this->_compile($this->_elseValue, $generator); + } + $parts[] = 'END'; + + return implode(' ', $parts); + } + + /** + * {@inheritDoc} + * + */ + public function traverse(callable $visitor) + { + foreach (['_conditions', '_values'] as $part) { + foreach ($this->{$part} as $c) { + if ($c instanceof ExpressionInterface) { + $visitor($c); + $c->traverse($visitor); + } + } + } + if ($this->_elseValue instanceof ExpressionInterface) { + $visitor($this->_elseValue); + $this->_elseValue->traverse($visitor); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/Comparison.php b/app/vendor/cakephp/cakephp/src/Database/Expression/Comparison.php new file mode 100644 index 000000000..25147e39f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Expression/Comparison.php @@ -0,0 +1,313 @@ +_type = $type; + } + + $this->setField($field); + $this->setValue($value); + $this->_operator = $operator; + } + + /** + * Sets the value + * + * @param mixed $value The value to compare + * @return void + */ + public function setValue($value) + { + $hasType = isset($this->_type) && is_string($this->_type); + $isMultiple = $hasType && strpos($this->_type, '[]') !== false; + + if ($hasType) { + $value = $this->_castToExpression($value, $this->_type); + } + + if ($isMultiple) { + list($value, $this->_valueExpressions) = $this->_collectExpressions($value); + } + + $this->_isMultiple = $isMultiple; + $this->_value = $value; + } + + /** + * Returns the value used for comparison + * + * @return mixed + */ + public function getValue() + { + return $this->_value; + } + + /** + * Sets the operator to use for the comparison + * + * @param string $operator The operator to be used for the comparison. + * @return void + */ + public function setOperator($operator) + { + $this->_operator = $operator; + } + + /** + * Returns the operator used for comparison + * + * @return string + */ + public function getOperator() + { + return $this->_operator; + } + + /** + * Convert the expression into a SQL fragment. + * + * @param \Cake\Database\ValueBinder $generator Placeholder generator object + * @return string + */ + public function sql(ValueBinder $generator) + { + $field = $this->_field; + + if ($field instanceof ExpressionInterface) { + $field = $field->sql($generator); + } + + if ($this->_value instanceof ExpressionInterface) { + $template = '%s %s (%s)'; + $value = $this->_value->sql($generator); + } else { + list($template, $value) = $this->_stringExpression($generator); + } + + return sprintf($template, $field, $this->_operator, $value); + } + + /** + * {@inheritDoc} + * + */ + public function traverse(callable $callable) + { + if ($this->_field instanceof ExpressionInterface) { + $callable($this->_field); + $this->_field->traverse($callable); + } + + if ($this->_value instanceof ExpressionInterface) { + $callable($this->_value); + $this->_value->traverse($callable); + } + + foreach ($this->_valueExpressions as $v) { + $callable($v); + $v->traverse($callable); + } + } + + /** + * Create a deep clone. + * + * Clones the field and value if they are expression objects. + * + * @return void + */ + public function __clone() + { + foreach (['_value', '_field'] as $prop) { + if ($prop instanceof ExpressionInterface) { + $this->{$prop} = clone $this->{$prop}; + } + } + } + + /** + * Returns a template and a placeholder for the value after registering it + * with the placeholder $generator + * + * @param \Cake\Database\ValueBinder $generator The value binder to use. + * @return array First position containing the template and the second a placeholder + */ + protected function _stringExpression($generator) + { + $template = '%s '; + + if ($this->_field instanceof ExpressionInterface) { + $template = '(%s) '; + } + + if ($this->_isMultiple) { + $template .= '%s (%s)'; + $type = str_replace('[]', '', $this->_type); + $value = $this->_flattenValue($this->_value, $generator, $type); + + // To avoid SQL errors when comparing a field to a list of empty values, + // better just throw an exception here + if ($value === '') { + $field = $this->_field instanceof ExpressionInterface ? $this->_field->sql($generator) : $this->_field; + throw new DatabaseException( + "Impossible to generate condition with empty list of values for field ($field)" + ); + } + } else { + $template .= '%s %s'; + $value = $this->_bindValue($this->_value, $generator, $this->_type); + } + + return [$template, $value]; + } + + /** + * Registers a value in the placeholder generator and returns the generated placeholder + * + * @param mixed $value The value to bind + * @param \Cake\Database\ValueBinder $generator The value binder to use + * @param string $type The type of $value + * @return string generated placeholder + */ + protected function _bindValue($value, $generator, $type) + { + $placeholder = $generator->placeholder('c'); + $generator->bind($placeholder, $value, $type); + + return $placeholder; + } + + /** + * Converts a traversable value into a set of placeholders generated by + * $generator and separated by `,` + * + * @param array|\Traversable $value the value to flatten + * @param \Cake\Database\ValueBinder $generator The value binder to use + * @param string|array|null $type the type to cast values to + * @return string + */ + protected function _flattenValue($value, $generator, $type = 'string') + { + $parts = []; + foreach ($this->_valueExpressions as $k => $v) { + $parts[$k] = $v->sql($generator); + unset($value[$k]); + } + + if (!empty($value)) { + $parts += $generator->generateManyNamed($value, $type); + } + + return implode(',', $parts); + } + + /** + * Returns an array with the original $values in the first position + * and all ExpressionInterface objects that could be found in the second + * position. + * + * @param array|\Traversable $values The rows to insert + * @return array + */ + protected function _collectExpressions($values) + { + if ($values instanceof ExpressionInterface) { + return [$values, []]; + } + + $expressions = $result = []; + $isArray = is_array($values); + + if ($isArray) { + $result = $values; + } + + foreach ($values as $k => $v) { + if ($v instanceof ExpressionInterface) { + $expressions[$k] = $v; + } + + if ($isArray) { + $result[$k] = $v; + } + } + + return [$result, $expressions]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/FieldInterface.php b/app/vendor/cakephp/cakephp/src/Database/Expression/FieldInterface.php new file mode 100644 index 000000000..36f753d14 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Expression/FieldInterface.php @@ -0,0 +1,38 @@ +_field = $field; + } + + /** + * Returns the field name + * + * @return string|\Cake\Database\ExpressionInterface + */ + public function getField() + { + return $this->_field; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/FunctionExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/FunctionExpression.php new file mode 100644 index 000000000..258b36801 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Expression/FunctionExpression.php @@ -0,0 +1,200 @@ + 'literal', ' rules']);` + * + * Will produce `CONCAT(name, ' rules')` + * + * @param string $name the name of the function to be constructed + * @param array $params list of arguments to be passed to the function + * If associative the key would be used as argument when value is 'literal' + * @param array $types associative array of types to be associated with the + * passed arguments + * @param string $returnType The return type of this expression + */ + public function __construct($name, $params = [], $types = [], $returnType = 'string') + { + $this->_name = $name; + $this->_returnType = $returnType; + parent::__construct($params, $types, ','); + } + + /** + * Sets the name of the SQL function to be invoke in this expression. + * + * @param string $name The name of the function + * @return $this + */ + public function setName($name) + { + $this->_name = $name; + + return $this; + } + + /** + * Gets the name of the SQL function to be invoke in this expression. + * + * @return string + */ + public function getName() + { + return $this->_name; + } + + /** + * Sets the name of the SQL function to be invoke in this expression, + * if no value is passed it will return current name + * + * @deprecated 3.4.0 Use setName()/getName() instead. + * @param string|null $name The name of the function + * @return string|$this + */ + public function name($name = null) + { + deprecationWarning( + 'FunctionExpression::name() is deprecated. ' . + 'Use FunctionExpression::setName()/getName() instead.' + ); + if ($name !== null) { + return $this->setName($name); + } + + return $this->getName(); + } + + /** + * Adds one or more arguments for the function call. + * + * @param array $params list of arguments to be passed to the function + * If associative the key would be used as argument when value is 'literal' + * @param array $types associative array of types to be associated with the + * passed arguments + * @param bool $prepend Whether to prepend or append to the list of arguments + * @see \Cake\Database\Expression\FunctionExpression::__construct() for more details. + * @return $this + */ + public function add($params, $types = [], $prepend = false) + { + $put = $prepend ? 'array_unshift' : 'array_push'; + $typeMap = $this->getTypeMap()->setTypes($types); + foreach ($params as $k => $p) { + if ($p === 'literal') { + $put($this->_conditions, $k); + continue; + } + + if ($p === 'identifier') { + $put($this->_conditions, new IdentifierExpression($k)); + continue; + } + + $type = $typeMap->type($k); + + if ($type !== null && !$p instanceof ExpressionInterface) { + $p = $this->_castToExpression($p, $type); + } + + if ($p instanceof ExpressionInterface) { + $put($this->_conditions, $p); + continue; + } + + $put($this->_conditions, ['value' => $p, 'type' => $type]); + } + + return $this; + } + + /** + * Returns the string representation of this object so that it can be used in a + * SQL query. Note that values condition values are not included in the string, + * in their place placeholders are put and can be replaced by the quoted values + * accordingly. + * + * @param \Cake\Database\ValueBinder $generator Placeholder generator object + * @return string + */ + public function sql(ValueBinder $generator) + { + $parts = []; + foreach ($this->_conditions as $condition) { + if ($condition instanceof ExpressionInterface) { + $condition = sprintf('%s', $condition->sql($generator)); + } elseif (is_array($condition)) { + $p = $generator->placeholder('param'); + $generator->bind($p, $condition['value'], $condition['type']); + $condition = $p; + } + $parts[] = $condition; + } + + return $this->_name . sprintf('(%s)', implode( + $this->_conjunction . ' ', + $parts + )); + } + + /** + * The name of the function is in itself an expression to generate, thus + * always adding 1 to the amount of expressions stored in this object. + * + * @return int + */ + public function count() + { + return 1 + count($this->_conditions); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/IdentifierExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/IdentifierExpression.php new file mode 100644 index 000000000..8aa9a209c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Expression/IdentifierExpression.php @@ -0,0 +1,90 @@ +_identifier = $identifier; + } + + /** + * Sets the identifier this expression represents + * + * @param string $identifier The identifier + * @return void + */ + public function setIdentifier($identifier) + { + $this->_identifier = $identifier; + } + + /** + * Returns the identifier this expression represents + * + * @return string + */ + public function getIdentifier() + { + return $this->_identifier; + } + + /** + * Converts the expression to its string representation + * + * @param \Cake\Database\ValueBinder $generator Placeholder generator object + * @return string + */ + public function sql(ValueBinder $generator) + { + return $this->_identifier; + } + + /** + * This method is a no-op, this is a leaf type of expression, + * hence there is nothing to traverse + * + * @param callable $callable The callable to traverse with. + * @return void + */ + public function traverse(callable $callable) + { + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/OrderByExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/OrderByExpression.php new file mode 100644 index 000000000..41a01cce9 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Expression/OrderByExpression.php @@ -0,0 +1,79 @@ +_conditions as $k => $direction) { + if ($direction instanceof ExpressionInterface) { + $direction = $direction->sql($generator); + } + $order[] = is_numeric($k) ? $direction : sprintf('%s %s', $k, $direction); + } + + return sprintf('ORDER BY %s', implode(', ', $order)); + } + + /** + * Auxiliary function used for decomposing a nested array of conditions and + * building a tree structure inside this object to represent the full SQL expression. + * + * New order by expressions are merged to existing ones + * + * @param array $orders list of order by expressions + * @param array $types list of types associated on fields referenced in $conditions + * @return void + */ + protected function _addConditions(array $orders, array $types) + { + foreach ($orders as $key => $val) { + if (is_string($key) && is_string($val) && !in_array(strtoupper($val), ['ASC', 'DESC'], true)) { + deprecationWarning( + 'Passing extra sort expressions by associative array is deprecated. ' . + 'Use QueryExpression or numeric array instead.' + ); + } + } + $this->_conditions = array_merge($this->_conditions, $orders); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/OrderClauseExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/OrderClauseExpression.php new file mode 100644 index 000000000..11f04de03 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Expression/OrderClauseExpression.php @@ -0,0 +1,81 @@ +_field = $field; + $this->_direction = strtolower($direction) === 'asc' ? 'ASC' : 'DESC'; + } + + /** + * {@inheritDoc} + */ + public function sql(ValueBinder $generator) + { + $field = $this->_field; + if ($field instanceof ExpressionInterface) { + $field = $field->sql($generator); + } + + return sprintf('%s %s', $field, $this->_direction); + } + + /** + * {@inheritDoc} + */ + public function traverse(callable $visitor) + { + if ($this->_field instanceof ExpressionInterface) { + $visitor($this->_field); + $this->_field->traverse($visitor); + } + } + + /** + * Create a deep clone of the order clause. + * + * @return void + */ + public function __clone() + { + if ($this->_field instanceof ExpressionInterface) { + $this->_field = clone $this->_field; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/QueryExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/QueryExpression.php new file mode 100644 index 000000000..26c63f538 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Expression/QueryExpression.php @@ -0,0 +1,827 @@ + :value" + * + * @var array + */ + protected $_conditions = []; + + /** + * Constructor. A new expression object can be created without any params and + * be built dynamically. Otherwise it is possible to pass an array of conditions + * containing either a tree-like array structure to be parsed and/or other + * expression objects. Optionally, you can set the conjunction keyword to be used + * for joining each part of this level of the expression tree. + * + * @param string|array|\Cake\Database\ExpressionInterface $conditions tree-like array structure containing all the conditions + * to be added or nested inside this expression object. + * @param array|\Cake\Database\TypeMap $types associative array of types to be associated with the values + * passed in $conditions. + * @param string $conjunction the glue that will join all the string conditions at this + * level of the expression tree. For example "AND", "OR", "XOR"... + * @see \Cake\Database\Expression\QueryExpression::add() for more details on $conditions and $types + */ + public function __construct($conditions = [], $types = [], $conjunction = 'AND') + { + $this->setTypeMap($types); + $this->setConjunction(strtoupper($conjunction)); + if (!empty($conditions)) { + $this->add($conditions, $this->getTypeMap()->getTypes()); + } + } + + /** + * Changes the conjunction for the conditions at this level of the expression tree. + * + * @param string $conjunction Value to be used for joining conditions + * @return $this + */ + public function setConjunction($conjunction) + { + $this->_conjunction = strtoupper($conjunction); + + return $this; + } + + /** + * Gets the currently configured conjunction for the conditions at this level of the expression tree. + * + * @return string + */ + public function getConjunction() + { + return $this->_conjunction; + } + + /** + * Changes the conjunction for the conditions at this level of the expression tree. + * If called with no arguments it will return the currently configured value. + * + * @deprecated 3.4.0 Use setConjunction()/getConjunction() instead. + * @param string|null $conjunction value to be used for joining conditions. If null it + * will not set any value, but return the currently stored one + * @return string|$this + */ + public function tieWith($conjunction = null) + { + deprecationWarning( + 'QueryExpression::tieWith() is deprecated. ' . + 'Use QueryExpression::setConjunction()/getConjunction() instead.' + ); + if ($conjunction !== null) { + return $this->setConjunction($conjunction); + } + + return $this->getConjunction(); + } + + /** + * Backwards compatible wrapper for tieWith() + * + * @param string|null $conjunction value to be used for joining conditions. If null it + * will not set any value, but return the currently stored one + * @return string|$this + * @deprecated 3.2.0 Use setConjunction()/getConjunction() instead + */ + public function type($conjunction = null) + { + deprecationWarning( + 'QueryExpression::type() is deprecated. ' . + 'Use QueryExpression::setConjunction()/getConjunction() instead.' + ); + + return $this->tieWith($conjunction); + } + + /** + * Adds one or more conditions to this expression object. Conditions can be + * expressed in a one dimensional array, that will cause all conditions to + * be added directly at this level of the tree or they can be nested arbitrarily + * making it create more expression objects that will be nested inside and + * configured to use the specified conjunction. + * + * If the type passed for any of the fields is expressed "type[]" (note braces) + * then it will cause the placeholder to be re-written dynamically so if the + * value is an array, it will create as many placeholders as values are in it. + * + * @param string|array|\Cake\Database\ExpressionInterface $conditions single or multiple conditions to + * be added. When using an array and the key is 'OR' or 'AND' a new expression + * object will be created with that conjunction and internal array value passed + * as conditions. + * @param array $types associative array of fields pointing to the type of the + * values that are being passed. Used for correctly binding values to statements. + * @see \Cake\Database\Query::where() for examples on conditions + * @return $this + */ + public function add($conditions, $types = []) + { + if (is_string($conditions)) { + $this->_conditions[] = $conditions; + + return $this; + } + + if ($conditions instanceof ExpressionInterface) { + $this->_conditions[] = $conditions; + + return $this; + } + + $this->_addConditions($conditions, $types); + + return $this; + } + + /** + * Adds a new condition to the expression object in the form "field = value". + * + * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * If it is suffixed with "[]" and the value is an array then multiple placeholders + * will be created, one per each value in the array. + * @return $this + */ + public function eq($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new Comparison($field, $value, $type, '=')); + } + + /** + * Adds a new condition to the expression object in the form "field != value". + * + * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * If it is suffixed with "[]" and the value is an array then multiple placeholders + * will be created, one per each value in the array. + * @return $this + */ + public function notEq($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new Comparison($field, $value, $type, '!=')); + } + + /** + * Adds a new condition to the expression object in the form "field > value". + * + * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function gt($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new Comparison($field, $value, $type, '>')); + } + + /** + * Adds a new condition to the expression object in the form "field < value". + * + * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function lt($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new Comparison($field, $value, $type, '<')); + } + + /** + * Adds a new condition to the expression object in the form "field >= value". + * + * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function gte($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new Comparison($field, $value, $type, '>=')); + } + + /** + * Adds a new condition to the expression object in the form "field <= value". + * + * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function lte($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new Comparison($field, $value, $type, '<=')); + } + + /** + * Adds a new condition to the expression object in the form "field IS NULL". + * + * @param string|\Cake\Database\ExpressionInterface $field database field to be + * tested for null + * @return $this + */ + public function isNull($field) + { + if (!($field instanceof ExpressionInterface)) { + $field = new IdentifierExpression($field); + } + + return $this->add(new UnaryExpression('IS NULL', $field, UnaryExpression::POSTFIX)); + } + + /** + * Adds a new condition to the expression object in the form "field IS NOT NULL". + * + * @param string|\Cake\Database\ExpressionInterface $field database field to be + * tested for not null + * @return $this + */ + public function isNotNull($field) + { + if (!($field instanceof ExpressionInterface)) { + $field = new IdentifierExpression($field); + } + + return $this->add(new UnaryExpression('IS NOT NULL', $field, UnaryExpression::POSTFIX)); + } + + /** + * Adds a new condition to the expression object in the form "field LIKE value". + * + * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function like($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new Comparison($field, $value, $type, 'LIKE')); + } + + /** + * Adds a new condition to the expression object in the form "field NOT LIKE value". + * + * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value + * @param mixed $value The value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function notLike($field, $value, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new Comparison($field, $value, $type, 'NOT LIKE')); + } + + /** + * Adds a new condition to the expression object in the form + * "field IN (value1, value2)". + * + * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value + * @param string|array $values the value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function in($field, $values, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + $type = $type ?: 'string'; + $type .= '[]'; + $values = $values instanceof ExpressionInterface ? $values : (array)$values; + + return $this->add(new Comparison($field, $values, $type, 'IN')); + } + + /** + * Adds a new case expression to the expression object + * + * @param array|\Cake\Database\ExpressionInterface $conditions The conditions to test. Must be a ExpressionInterface + * instance, or an array of ExpressionInterface instances. + * @param array|\Cake\Database\ExpressionInterface $values associative array of values to be associated with the conditions + * passed in $conditions. If there are more $values than $conditions, the last $value is used as the `ELSE` value + * @param array $types associative array of types to be associated with the values + * passed in $values + * @return $this + */ + public function addCase($conditions, $values = [], $types = []) + { + return $this->add(new CaseExpression($conditions, $values, $types)); + } + + /** + * Adds a new condition to the expression object in the form + * "field NOT IN (value1, value2)". + * + * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value + * @param array $values the value to be bound to $field for comparison + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function notIn($field, $values, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + $type = $type ?: 'string'; + $type .= '[]'; + $values = $values instanceof ExpressionInterface ? $values : (array)$values; + + return $this->add(new Comparison($field, $values, $type, 'NOT IN')); + } + + /** + * Adds a new condition to the expression object in the form "EXISTS (...)". + * + * @param \Cake\Database\ExpressionInterface $query the inner query + * @return $this + */ + public function exists(ExpressionInterface $query) + { + return $this->add(new UnaryExpression('EXISTS', $query, UnaryExpression::PREFIX)); + } + + /** + * Adds a new condition to the expression object in the form "NOT EXISTS (...)". + * + * @param \Cake\Database\ExpressionInterface $query the inner query + * @return $this + */ + public function notExists(ExpressionInterface $query) + { + return $this->add(new UnaryExpression('NOT EXISTS', $query, UnaryExpression::PREFIX)); + } + + /** + * Adds a new condition to the expression object in the form + * "field BETWEEN from AND to". + * + * @param string|\Cake\Database\ExpressionInterface $field The field name to compare for values in between the range. + * @param mixed $from The initial value of the range. + * @param mixed $to The ending value in the comparison range. + * @param string|null $type the type name for $value as configured using the Type map. + * @return $this + */ + public function between($field, $from, $to, $type = null) + { + if ($type === null) { + $type = $this->_calculateType($field); + } + + return $this->add(new BetweenExpression($field, $from, $to, $type)); + } + +// @codingStandardsIgnoreStart + /** + * Returns a new QueryExpression object containing all the conditions passed + * and set up the conjunction to be "AND" + * + * @param string|array|\Cake\Database\ExpressionInterface $conditions to be joined with AND + * @param array $types associative array of fields pointing to the type of the + * values that are being passed. Used for correctly binding values to statements. + * @return \Cake\Database\Expression\QueryExpression + */ + public function and_($conditions, $types = []) + { + if ($this->isCallable($conditions)) { + return $conditions(new static([], $this->getTypeMap()->setTypes($types))); + } + + return new static($conditions, $this->getTypeMap()->setTypes($types)); + } + + /** + * Returns a new QueryExpression object containing all the conditions passed + * and set up the conjunction to be "OR" + * + * @param string|array|\Cake\Database\ExpressionInterface $conditions to be joined with OR + * @param array $types associative array of fields pointing to the type of the + * values that are being passed. Used for correctly binding values to statements. + * @return \Cake\Database\Expression\QueryExpression + */ + public function or_($conditions, $types = []) + { + if ($this->isCallable($conditions)) { + return $conditions(new static([], $this->getTypeMap()->setTypes($types), 'OR')); + } + + return new static($conditions, $this->getTypeMap()->setTypes($types), 'OR'); + } +// @codingStandardsIgnoreEnd + + /** + * Adds a new set of conditions to this level of the tree and negates + * the final result by prepending a NOT, it will look like + * "NOT ( (condition1) AND (conditions2) )" conjunction depends on the one + * currently configured for this object. + * + * @param string|array|\Cake\Database\ExpressionInterface $conditions to be added and negated + * @param array $types associative array of fields pointing to the type of the + * values that are being passed. Used for correctly binding values to statements. + * @return $this + */ + public function not($conditions, $types = []) + { + return $this->add(['NOT' => $conditions], $types); + } + + /** + * Returns the number of internal conditions that are stored in this expression. + * Useful to determine if this expression object is void or it will generate + * a non-empty string when compiled + * + * @return int + */ + public function count() + { + return count($this->_conditions); + } + + /** + * Builds equal condition or assignment with identifier wrapping. + * + * @param string $left Left join condition field name. + * @param string $right Right join condition field name. + * @return $this + */ + public function equalFields($left, $right) + { + $wrapIdentifier = function ($field) { + if ($field instanceof ExpressionInterface) { + return $field; + } + + return new IdentifierExpression($field); + }; + + return $this->eq($wrapIdentifier($left), $wrapIdentifier($right)); + } + + /** + * Returns the string representation of this object so that it can be used in a + * SQL query. Note that values condition values are not included in the string, + * in their place placeholders are put and can be replaced by the quoted values + * accordingly. + * + * @param \Cake\Database\ValueBinder $generator Placeholder generator object + * @return string + */ + public function sql(ValueBinder $generator) + { + $len = $this->count(); + if ($len === 0) { + return ''; + } + $conjunction = $this->_conjunction; + $template = ($len === 1) ? '%s' : '(%s)'; + $parts = []; + foreach ($this->_conditions as $part) { + if ($part instanceof Query) { + $part = '(' . $part->sql($generator) . ')'; + } elseif ($part instanceof ExpressionInterface) { + $part = $part->sql($generator); + } + if (strlen($part)) { + $parts[] = $part; + } + } + + return sprintf($template, implode(" $conjunction ", $parts)); + } + + /** + * Traverses the tree structure of this query expression by executing a callback + * function for each of the conditions that are included in this object. + * Useful for compiling the final expression, or doing + * introspection in the structure. + * + * Callback function receives as only argument an instance of ExpressionInterface + * + * @param callable $callable The callable to apply to all sub-expressions. + * @return void + */ + public function traverse(callable $callable) + { + foreach ($this->_conditions as $c) { + if ($c instanceof ExpressionInterface) { + $callable($c); + $c->traverse($callable); + } + } + } + + /** + * Executes a callable function for each of the parts that form this expression. + * + * The callable function is required to return a value with which the currently + * visited part will be replaced. If the callable function returns null then + * the part will be discarded completely from this expression. + * + * The callback function will receive each of the conditions as first param and + * the key as second param. It is possible to declare the second parameter as + * passed by reference, this will enable you to change the key under which the + * modified part is stored. + * + * @param callable $callable The callable to apply to each part. + * @return $this + */ + public function iterateParts(callable $callable) + { + $parts = []; + foreach ($this->_conditions as $k => $c) { + $key =& $k; + $part = $callable($c, $key); + if ($part !== null) { + $parts[$key] = $part; + } + } + $this->_conditions = $parts; + + return $this; + } + + /** + * Helps calling the `and()` and `or()` methods transparently. + * + * @param string $method The method name. + * @param array $args The arguments to pass to the method. + * @return \Cake\Database\Expression\QueryExpression + * @throws \BadMethodCallException + */ + public function __call($method, $args) + { + if (in_array($method, ['and', 'or'])) { + return call_user_func_array([$this, $method . '_'], $args); + } + throw new BadMethodCallException(sprintf('Method %s does not exist', $method)); + } + + /** + * Check whether or not a callable is acceptable. + * + * We don't accept ['class', 'method'] style callbacks, + * as they often contain user input and arrays of strings + * are easy to sneak in. + * + * @param callable $c The callable to check. + * @return bool Valid callable. + */ + public function isCallable($c) + { + if (is_string($c)) { + return false; + } + if (is_object($c) && is_callable($c)) { + return true; + } + + return is_array($c) && isset($c[0]) && is_object($c[0]) && is_callable($c); + } + + /** + * Returns true if this expression contains any other nested + * ExpressionInterface objects + * + * @return bool + */ + public function hasNestedExpression() + { + foreach ($this->_conditions as $c) { + if ($c instanceof ExpressionInterface) { + return true; + } + } + + return false; + } + + /** + * Auxiliary function used for decomposing a nested array of conditions and build + * a tree structure inside this object to represent the full SQL expression. + * String conditions are stored directly in the conditions, while any other + * representation is wrapped around an adequate instance or of this class. + * + * @param array $conditions list of conditions to be stored in this object + * @param array $types list of types associated on fields referenced in $conditions + * @return void + */ + protected function _addConditions(array $conditions, array $types) + { + $operators = ['and', 'or', 'xor']; + + $typeMap = $this->getTypeMap()->setTypes($types); + + foreach ($conditions as $k => $c) { + $numericKey = is_numeric($k); + + if ($this->isCallable($c)) { + $expr = new static([], $typeMap); + $c = $c($expr, $this); + } + + if ($numericKey && empty($c)) { + continue; + } + + $isArray = is_array($c); + $isOperator = in_array(strtolower($k), $operators); + $isNot = strtolower($k) === 'not'; + + if (($isOperator || $isNot) && ($isArray || $c instanceof Countable) && count($c) === 0) { + continue; + } + + if ($numericKey && $c instanceof ExpressionInterface) { + $this->_conditions[] = $c; + continue; + } + + if ($numericKey && is_string($c)) { + $this->_conditions[] = $c; + continue; + } + + if ($numericKey && $isArray || $isOperator) { + $this->_conditions[] = new static($c, $typeMap, $numericKey ? 'AND' : $k); + continue; + } + + if ($isNot) { + $this->_conditions[] = new UnaryExpression('NOT', new static($c, $typeMap)); + continue; + } + + if (!$numericKey) { + $this->_conditions[] = $this->_parseCondition($k, $c); + } + } + } + + /** + * Parses a string conditions by trying to extract the operator inside it if any + * and finally returning either an adequate QueryExpression object or a plain + * string representation of the condition. This function is responsible for + * generating the placeholders and replacing the values by them, while storing + * the value elsewhere for future binding. + * + * @param string $field The value from with the actual field and operator will + * be extracted. + * @param mixed $value The value to be bound to a placeholder for the field + * @return string|\Cake\Database\ExpressionInterface + */ + protected function _parseCondition($field, $value) + { + $operator = '='; + $expression = $field; + $parts = explode(' ', trim($field), 2); + + if (count($parts) > 1) { + list($expression, $operator) = $parts; + } + + $type = $this->getTypeMap()->type($expression); + $operator = strtolower(trim($operator)); + + $typeMultiple = strpos($type, '[]') !== false; + if (in_array($operator, ['in', 'not in']) || $typeMultiple) { + $type = $type ?: 'string'; + $type .= $typeMultiple ? null : '[]'; + $operator = $operator === '=' ? 'IN' : $operator; + $operator = $operator === '!=' ? 'NOT IN' : $operator; + $typeMultiple = true; + } + + if ($typeMultiple) { + $value = $value instanceof ExpressionInterface ? $value : (array)$value; + } + + if ($operator === 'is' && $value === null) { + return new UnaryExpression( + 'IS NULL', + new IdentifierExpression($expression), + UnaryExpression::POSTFIX + ); + } + + if ($operator === 'is not' && $value === null) { + return new UnaryExpression( + 'IS NOT NULL', + new IdentifierExpression($expression), + UnaryExpression::POSTFIX + ); + } + + if ($operator === 'is' && $value !== null) { + $operator = '='; + } + + if ($operator === 'is not' && $value !== null) { + $operator = '!='; + } + + return new Comparison($expression, $value, $type, $operator); + } + + /** + * Returns the type name for the passed field if it was stored in the typeMap + * + * @param string|\Cake\Database\Expression\IdentifierExpression $field The field name to get a type for. + * @return string|null The computed type or null, if the type is unknown. + */ + protected function _calculateType($field) + { + $field = $field instanceof IdentifierExpression ? $field->getIdentifier() : $field; + if (is_string($field)) { + return $this->getTypeMap()->type($field); + } + + return null; + } + + /** + * Clone this object and its subtree of expressions. + * + * @return void + */ + public function __clone() + { + foreach ($this->_conditions as $i => $condition) { + if ($condition instanceof ExpressionInterface) { + $this->_conditions[$i] = clone $condition; + } + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/TupleComparison.php b/app/vendor/cakephp/cakephp/src/Database/Expression/TupleComparison.php new file mode 100644 index 000000000..40ea6648f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Expression/TupleComparison.php @@ -0,0 +1,192 @@ +_type = (array)$types; + } + + /** + * Convert the expression into a SQL fragment. + * + * @param \Cake\Database\ValueBinder $generator Placeholder generator object + * @return string + */ + public function sql(ValueBinder $generator) + { + $template = '(%s) %s (%s)'; + $fields = []; + $originalFields = $this->getField(); + + if (!is_array($originalFields)) { + $originalFields = [$originalFields]; + } + + foreach ($originalFields as $field) { + $fields[] = $field instanceof ExpressionInterface ? $field->sql($generator) : $field; + } + + $values = $this->_stringifyValues($generator); + + $field = implode(', ', $fields); + + return sprintf($template, $field, $this->_operator, $values); + } + + /** + * Returns a string with the values as placeholders in a string to be used + * for the SQL version of this expression + * + * @param \Cake\Database\ValueBinder $generator The value binder to convert expressions with. + * @return string + */ + protected function _stringifyValues($generator) + { + $values = []; + $parts = $this->getValue(); + + if ($parts instanceof ExpressionInterface) { + return $parts->sql($generator); + } + + foreach ($parts as $i => $value) { + if ($value instanceof ExpressionInterface) { + $values[] = $value->sql($generator); + continue; + } + + $type = $this->_type; + $multiType = is_array($type); + $isMulti = $this->isMulti(); + $type = $multiType ? $type : str_replace('[]', '', $type); + $type = $type ?: null; + + if ($isMulti) { + $bound = []; + foreach ($value as $k => $val) { + $valType = $multiType ? $type[$k] : $type; + $bound[] = $this->_bindValue($generator, $val, $valType); + } + + $values[] = sprintf('(%s)', implode(',', $bound)); + continue; + } + + $valType = $multiType && isset($type[$i]) ? $type[$i] : $type; + $values[] = $this->_bindValue($generator, $value, $valType); + } + + return implode(', ', $values); + } + + /** + * Registers a value in the placeholder generator and returns the generated + * placeholder + * + * @param \Cake\Database\ValueBinder $generator The value binder + * @param mixed $value The value to bind + * @param string $type The type to use + * @return string generated placeholder + */ + protected function _bindValue($generator, $value, $type) + { + $placeholder = $generator->placeholder('tuple'); + $generator->bind($placeholder, $value, $type); + + return $placeholder; + } + + /** + * Traverses the tree of expressions stored in this object, visiting first + * expressions in the left hand side and then the rest. + * + * Callback function receives as its only argument an instance of an ExpressionInterface + * + * @param callable $callable The callable to apply to sub-expressions + * @return void + */ + public function traverse(callable $callable) + { + foreach ($this->getField() as $field) { + $this->_traverseValue($field, $callable); + } + + $value = $this->getValue(); + if ($value instanceof ExpressionInterface) { + $callable($value); + $value->traverse($callable); + + return; + } + + foreach ($value as $i => $val) { + if ($this->isMulti()) { + foreach ($val as $v) { + $this->_traverseValue($v, $callable); + } + } else { + $this->_traverseValue($val, $callable); + } + } + } + + /** + * Conditionally executes the callback for the passed value if + * it is an ExpressionInterface + * + * @param mixed $value The value to traverse + * @param callable $callable The callable to use when traversing + * @return void + */ + protected function _traverseValue($value, $callable) + { + if ($value instanceof ExpressionInterface) { + $callable($value); + $value->traverse($callable); + } + } + + /** + * Determines if each of the values in this expressions is a tuple in + * itself + * + * @return bool + */ + public function isMulti() + { + return in_array(strtolower($this->_operator), ['in', 'not in']); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/UnaryExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/UnaryExpression.php new file mode 100644 index 000000000..f58fd675c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Expression/UnaryExpression.php @@ -0,0 +1,116 @@ +_operator = $operator; + $this->_value = $value; + $this->_mode = $mode; + } + + /** + * Converts the expression to its string representation + * + * @param \Cake\Database\ValueBinder $generator Placeholder generator object + * @return string + */ + public function sql(ValueBinder $generator) + { + $operand = $this->_value; + if ($operand instanceof ExpressionInterface) { + $operand = $operand->sql($generator); + } + + if ($this->_mode === self::POSTFIX) { + return '(' . $operand . ') ' . $this->_operator; + } + + return $this->_operator . ' (' . $operand . ')'; + } + + /** + * {@inheritDoc} + * + */ + public function traverse(callable $callable) + { + if ($this->_value instanceof ExpressionInterface) { + $callable($this->_value); + $this->_value->traverse($callable); + } + } + + /** + * Perform a deep clone of the inner expression. + * + * @return void + */ + public function __clone() + { + if ($this->_value instanceof ExpressionInterface) { + $this->_value = clone $this->_value; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/ValuesExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/ValuesExpression.php new file mode 100644 index 000000000..261e512d5 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Expression/ValuesExpression.php @@ -0,0 +1,385 @@ + type names + */ + public function __construct(array $columns, $typeMap) + { + $this->_columns = $columns; + $this->setTypeMap($typeMap); + } + + /** + * Add a row of data to be inserted. + * + * @param array|\Cake\Database\Query $data Array of data to append into the insert, or + * a query for doing INSERT INTO .. SELECT style commands + * @return void + * @throws \Cake\Database\Exception When mixing array + Query data types. + */ + public function add($data) + { + if ((count($this->_values) && $data instanceof Query) || + ($this->_query && is_array($data)) + ) { + throw new Exception( + 'You cannot mix subqueries and array data in inserts.' + ); + } + if ($data instanceof Query) { + $this->setQuery($data); + + return; + } + $this->_values[] = $data; + $this->_castedExpressions = false; + } + + /** + * Sets the columns to be inserted. + * + * @param array $cols Array with columns to be inserted. + * @return $this + */ + public function setColumns($cols) + { + $this->_columns = $cols; + $this->_castedExpressions = false; + + return $this; + } + + /** + * Gets the columns to be inserted. + * + * @return array + */ + public function getColumns() + { + return $this->_columns; + } + + /** + * Sets the columns to be inserted. If no params are passed, then it returns + * the currently stored columns. + * + * @deprecated 3.4.0 Use setColumns()/getColumns() instead. + * @param array|null $cols Array with columns to be inserted. + * @return array|$this + */ + public function columns($cols = null) + { + deprecationWarning( + 'ValuesExpression::columns() is deprecated. ' . + 'Use ValuesExpression::setColumns()/getColumns() instead.' + ); + if ($cols !== null) { + return $this->setColumns($cols); + } + + return $this->getColumns(); + } + + /** + * Get the bare column names. + * + * Because column names could be identifier quoted, we + * need to strip the identifiers off of the columns. + * + * @return array + */ + protected function _columnNames() + { + $columns = []; + foreach ($this->_columns as $col) { + if (is_string($col)) { + $col = trim($col, '`[]"'); + } + $columns[] = $col; + } + + return $columns; + } + + /** + * Sets the values to be inserted. + * + * @param array $values Array with values to be inserted. + * @return $this + */ + public function setValues($values) + { + $this->_values = $values; + $this->_castedExpressions = false; + + return $this; + } + + /** + * Gets the values to be inserted. + * + * @return array + */ + public function getValues() + { + if (!$this->_castedExpressions) { + $this->_processExpressions(); + } + + return $this->_values; + } + + /** + * Sets the values to be inserted. If no params are passed, then it returns + * the currently stored values + * + * @deprecated 3.4.0 Use setValues()/getValues() instead. + * @param array|null $values Array with values to be inserted. + * @return array|$this + */ + public function values($values = null) + { + deprecationWarning( + 'ValuesExpression::values() is deprecated. ' . + 'Use ValuesExpression::setValues()/getValues() instead.' + ); + if ($values !== null) { + return $this->setValues($values); + } + + return $this->getValues(); + } + + /** + * Sets the query object to be used as the values expression to be evaluated + * to insert records in the table. + * + * @param \Cake\Database\Query $query The query to set + * @return $this + */ + public function setQuery(Query $query) + { + $this->_query = $query; + + return $this; + } + + /** + * Gets the query object to be used as the values expression to be evaluated + * to insert records in the table. + * + * @return \Cake\Database\Query|null + */ + public function getQuery() + { + return $this->_query; + } + + /** + * Sets the query object to be used as the values expression to be evaluated + * to insert records in the table. If no params are passed, then it returns + * the currently stored query + * + * @deprecated 3.4.0 Use setQuery()/getQuery() instead. + * @param \Cake\Database\Query|null $query The query to set + * @return \Cake\Database\Query|null|$this + */ + public function query(Query $query = null) + { + deprecationWarning( + 'ValuesExpression::query() is deprecated. ' . + 'Use ValuesExpression::setQuery()/getQuery() instead.' + ); + if ($query !== null) { + return $this->setQuery($query); + } + + return $this->getQuery(); + } + + /** + * Convert the values into a SQL string with placeholders. + * + * @param \Cake\Database\ValueBinder $generator Placeholder generator object + * @return string + */ + public function sql(ValueBinder $generator) + { + if (empty($this->_values) && empty($this->_query)) { + return ''; + } + + if (!$this->_castedExpressions) { + $this->_processExpressions(); + } + + $columns = $this->_columnNames(); + $defaults = array_fill_keys($columns, null); + $placeholders = []; + + $types = []; + $typeMap = $this->getTypeMap(); + foreach ($defaults as $col => $v) { + $types[$col] = $typeMap->type($col); + } + + foreach ($this->_values as $row) { + $row += $defaults; + $rowPlaceholders = []; + + foreach ($columns as $column) { + $value = $row[$column]; + + if ($value instanceof ExpressionInterface) { + $rowPlaceholders[] = '(' . $value->sql($generator) . ')'; + continue; + } + + $placeholder = $generator->placeholder('c'); + $rowPlaceholders[] = $placeholder; + $generator->bind($placeholder, $value, $types[$column]); + } + + $placeholders[] = implode(', ', $rowPlaceholders); + } + + if ($this->getQuery()) { + return ' ' . $this->getQuery()->sql($generator); + } + + return sprintf(' VALUES (%s)', implode('), (', $placeholders)); + } + + /** + * Traverse the values expression. + * + * This method will also traverse any queries that are to be used in the INSERT + * values. + * + * @param callable $visitor The visitor to traverse the expression with. + * @return void + */ + public function traverse(callable $visitor) + { + if ($this->_query) { + return; + } + + if (!$this->_castedExpressions) { + $this->_processExpressions(); + } + + foreach ($this->_values as $v) { + if ($v instanceof ExpressionInterface) { + $v->traverse($visitor); + } + if (!is_array($v)) { + continue; + } + foreach ($v as $column => $field) { + if ($field instanceof ExpressionInterface) { + $visitor($field); + $field->traverse($visitor); + } + } + } + } + + /** + * Converts values that need to be casted to expressions + * + * @return void + */ + protected function _processExpressions() + { + $types = []; + $typeMap = $this->getTypeMap(); + + $columns = $this->_columnNames(); + foreach ($columns as $c) { + if (!is_scalar($c)) { + continue; + } + $types[$c] = $typeMap->type($c); + } + + $types = $this->_requiresToExpressionCasting($types); + + if (empty($types)) { + return; + } + + foreach ($this->_values as $row => $values) { + foreach ($types as $col => $type) { + /* @var \Cake\Database\Type\ExpressionTypeInterface $type */ + $this->_values[$row][$col] = $type->toExpression($values[$col]); + } + } + $this->_castedExpressions = true; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/ExpressionInterface.php b/app/vendor/cakephp/cakephp/src/Database/ExpressionInterface.php new file mode 100644 index 000000000..57b6667db --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/ExpressionInterface.php @@ -0,0 +1,41 @@ +_driver = $driver; + $map = $typeMap->toArray(); + $types = Type::buildAll(); + + $simpleMap = $batchingMap = []; + $simpleResult = $batchingResult = []; + + foreach ($types as $k => $type) { + if ($type instanceof OptionalConvertInterface && !$type->requiresToPhpCast()) { + continue; + } + + // Because of backwards compatibility reasons, we won't allow classes + // inheriting Type in userland code to be batchable, even if they implement + // the interface. Users can implement the TypeInterface instead to have + // access to this feature. + $batchingType = $type instanceof BatchCastingInterface && + !($type instanceof Type && + strpos(get_class($type), 'Cake\Database\Type') === false); + + if ($batchingType) { + $batchingMap[$k] = $type; + continue; + } + + $simpleMap[$k] = $type; + } + + foreach ($map as $field => $type) { + if (isset($simpleMap[$type])) { + $simpleResult[$field] = $simpleMap[$type]; + continue; + } + if (isset($batchingMap[$type])) { + $batchingResult[$type][] = $field; + } + } + + // Using batching when there is only a couple for the type is actually slower, + // so, let's check for that case here. + foreach ($batchingResult as $type => $fields) { + if (count($fields) > 2) { + continue; + } + + foreach ($fields as $f) { + $simpleResult[$f] = $batchingMap[$type]; + } + unset($batchingResult[$type]); + } + + $this->types = $types; + $this->_typeMap = $simpleResult; + $this->batchingTypeMap = $batchingResult; + } + + /** + * Converts each of the fields in the array that are present in the type map + * using the corresponding Type class. + * + * @param array $row The array with the fields to be casted + * @return array + */ + public function __invoke($row) + { + if (!empty($this->_typeMap)) { + foreach ($this->_typeMap as $field => $type) { + $row[$field] = $type->toPHP($row[$field], $this->_driver); + } + } + + if (!empty($this->batchingTypeMap)) { + foreach ($this->batchingTypeMap as $t => $fields) { + $row = $this->types[$t]->manyToPHP($row, $fields, $this->_driver); + } + } + + return $row; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/FunctionsBuilder.php b/app/vendor/cakephp/cakephp/src/Database/FunctionsBuilder.php new file mode 100644 index 000000000..0bab11d24 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/FunctionsBuilder.php @@ -0,0 +1,284 @@ + 'literal']; + } + + return $this->_build($name, $expression, $types, $return); + } + + /** + * Returns a FunctionExpression representing a call to SQL SUM function. + * + * @param mixed $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function sum($expression, $types = []) + { + $returnType = 'float'; + if (current($types) === 'integer') { + $returnType = 'integer'; + } + + return $this->_literalArgumentFunction('SUM', $expression, $types, $returnType); + } + + /** + * Returns a FunctionExpression representing a call to SQL AVG function. + * + * @param mixed $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function avg($expression, $types = []) + { + return $this->_literalArgumentFunction('AVG', $expression, $types, 'float'); + } + + /** + * Returns a FunctionExpression representing a call to SQL MAX function. + * + * @param mixed $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function max($expression, $types = []) + { + return $this->_literalArgumentFunction('MAX', $expression, $types, current($types) ?: 'string'); + } + + /** + * Returns a FunctionExpression representing a call to SQL MIN function. + * + * @param mixed $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function min($expression, $types = []) + { + return $this->_literalArgumentFunction('MIN', $expression, $types, current($types) ?: 'string'); + } + + /** + * Returns a FunctionExpression representing a call to SQL COUNT function. + * + * @param mixed $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function count($expression, $types = []) + { + return $this->_literalArgumentFunction('COUNT', $expression, $types, 'integer'); + } + + /** + * Returns a FunctionExpression representing a string concatenation + * + * @param array $args List of strings or expressions to concatenate + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function concat($args, $types = []) + { + return $this->_build('CONCAT', $args, $types, 'string'); + } + + /** + * Returns a FunctionExpression representing a call to SQL COALESCE function. + * + * @param array $args List of expressions to evaluate as function parameters + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function coalesce($args, $types = []) + { + return $this->_build('COALESCE', $args, $types, current($types) ?: 'string'); + } + + /** + * Returns a FunctionExpression representing the difference in days between + * two dates. + * + * @param array $args List of expressions to obtain the difference in days. + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function dateDiff($args, $types = []) + { + return $this->_build('DATEDIFF', $args, $types, 'integer'); + } + + /** + * Returns the specified date part from the SQL expression. + * + * @param string $part Part of the date to return. + * @param string $expression Expression to obtain the date part from. + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function datePart($part, $expression, $types = []) + { + return $this->extract($part, $expression); + } + + /** + * Returns the specified date part from the SQL expression. + * + * @param string $part Part of the date to return. + * @param string $expression Expression to obtain the date part from. + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function extract($part, $expression, $types = []) + { + $expression = $this->_literalArgumentFunction('EXTRACT', $expression, $types, 'integer'); + $expression->setConjunction(' FROM')->add([$part => 'literal'], [], true); + + return $expression; + } + + /** + * Add the time unit to the date expression + * + * @param string $expression Expression to obtain the date part from. + * @param string $value Value to be added. Use negative to substract. + * @param string $unit Unit of the value e.g. hour or day. + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function dateAdd($expression, $value, $unit, $types = []) + { + if (!is_numeric($value)) { + $value = 0; + } + $interval = $value . ' ' . $unit; + $expression = $this->_literalArgumentFunction('DATE_ADD', $expression, $types, 'datetime'); + $expression->setConjunction(', INTERVAL')->add([$interval => 'literal']); + + return $expression; + } + + /** + * Returns a FunctionExpression representing a call to SQL WEEKDAY function. + * 1 - Sunday, 2 - Monday, 3 - Tuesday... + * + * @param mixed $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function dayOfWeek($expression, $types = []) + { + return $this->_literalArgumentFunction('DAYOFWEEK', $expression, $types, 'integer'); + } + + /** + * Returns a FunctionExpression representing a call to SQL WEEKDAY function. + * 1 - Sunday, 2 - Monday, 3 - Tuesday... + * + * @param mixed $expression the function argument + * @param array $types list of types to bind to the arguments + * @return \Cake\Database\Expression\FunctionExpression + */ + public function weekday($expression, $types = []) + { + return $this->dayOfWeek($expression, $types); + } + + /** + * Returns a FunctionExpression representing a call that will return the current + * date and time. By default it returns both date and time, but you can also + * make it generate only the date or only the time. + * + * @param string $type (datetime|date|time) + * @return \Cake\Database\Expression\FunctionExpression + */ + public function now($type = 'datetime') + { + if ($type === 'datetime') { + return $this->_build('NOW')->setReturnType('datetime'); + } + if ($type === 'date') { + return $this->_build('CURRENT_DATE')->setReturnType('date'); + } + if ($type === 'time') { + return $this->_build('CURRENT_TIME')->setReturnType('time'); + } + } + + /** + * Magic method dispatcher to create custom SQL function calls + * + * @param string $name the SQL function name to construct + * @param array $args list with up to 3 arguments, first one being an array with + * parameters for the SQL function, the second one a list of types to bind to those + * params, and the third one the return type of the function + * @return \Cake\Database\Expression\FunctionExpression + */ + public function __call($name, $args) + { + switch (count($args)) { + case 0: + return $this->_build($name); + case 1: + return $this->_build($name, $args[0]); + case 2: + return $this->_build($name, $args[0], $args[1]); + default: + return $this->_build($name, $args[0], $args[1], $args[2]); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/IdentifierQuoter.php b/app/vendor/cakephp/cakephp/src/Database/IdentifierQuoter.php new file mode 100644 index 000000000..f5866e85c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/IdentifierQuoter.php @@ -0,0 +1,264 @@ +_driver = $driver; + } + + /** + * Iterates over each of the clauses in a query looking for identifiers and + * quotes them + * + * @param \Cake\Database\Query $query The query to have its identifiers quoted + * @return \Cake\Database\Query + */ + public function quote(Query $query) + { + $binder = $query->getValueBinder(); + $query->setValueBinder(false); + + if ($query->type() === 'insert') { + $this->_quoteInsert($query); + } elseif ($query->type() === 'update') { + $this->_quoteUpdate($query); + } else { + $this->_quoteParts($query); + } + + $query->traverseExpressions([$this, 'quoteExpression']); + $query->setValueBinder($binder); + + return $query; + } + + /** + * Quotes identifiers inside expression objects + * + * @param \Cake\Database\ExpressionInterface $expression The expression object to walk and quote. + * @return void + */ + public function quoteExpression($expression) + { + if ($expression instanceof FieldInterface) { + $this->_quoteComparison($expression); + + return; + } + + if ($expression instanceof OrderByExpression) { + $this->_quoteOrderBy($expression); + + return; + } + + if ($expression instanceof IdentifierExpression) { + $this->_quoteIdentifierExpression($expression); + + return; + } + } + + /** + * Quotes all identifiers in each of the clauses of a query + * + * @param \Cake\Database\Query $query The query to quote. + * @return void + */ + protected function _quoteParts($query) + { + foreach (['distinct', 'select', 'from', 'group'] as $part) { + $contents = $query->clause($part); + + if (!is_array($contents)) { + continue; + } + + $result = $this->_basicQuoter($contents); + if (!empty($result)) { + $query->{$part}($result, true); + } + } + + $joins = $query->clause('join'); + if ($joins) { + $joins = $this->_quoteJoins($joins); + $query->join($joins, [], true); + } + } + + /** + * A generic identifier quoting function used for various parts of the query + * + * @param array $part the part of the query to quote + * @return array + */ + protected function _basicQuoter($part) + { + $result = []; + foreach ((array)$part as $alias => $value) { + $value = !is_string($value) ? $value : $this->_driver->quoteIdentifier($value); + $alias = is_numeric($alias) ? $alias : $this->_driver->quoteIdentifier($alias); + $result[$alias] = $value; + } + + return $result; + } + + /** + * Quotes both the table and alias for an array of joins as stored in a Query + * object + * + * @param array $joins The joins to quote. + * @return array + */ + protected function _quoteJoins($joins) + { + $result = []; + foreach ($joins as $value) { + $alias = null; + if (!empty($value['alias'])) { + $alias = $this->_driver->quoteIdentifier($value['alias']); + $value['alias'] = $alias; + } + + if (is_string($value['table'])) { + $value['table'] = $this->_driver->quoteIdentifier($value['table']); + } + + $result[$alias] = $value; + } + + return $result; + } + + /** + * Quotes the table name and columns for an insert query + * + * @param \Cake\Database\Query $query The insert query to quote. + * @return void + */ + protected function _quoteInsert($query) + { + list($table, $columns) = $query->clause('insert'); + $table = $this->_driver->quoteIdentifier($table); + foreach ($columns as &$column) { + if (is_scalar($column)) { + $column = $this->_driver->quoteIdentifier($column); + } + } + $query->insert($columns)->into($table); + } + + /** + * Quotes the table name for an update query + * + * @param \Cake\Database\Query $query The update query to quote. + * @return void + */ + protected function _quoteUpdate($query) + { + $table = $query->clause('update')[0]; + + if (is_string($table)) { + $query->update($this->_driver->quoteIdentifier($table)); + } + } + + /** + * Quotes identifiers in expression objects implementing the field interface + * + * @param \Cake\Database\Expression\FieldInterface $expression The expression to quote. + * @return void + */ + protected function _quoteComparison(FieldInterface $expression) + { + $field = $expression->getField(); + if (is_string($field)) { + $expression->setField($this->_driver->quoteIdentifier($field)); + } elseif (is_array($field)) { + $quoted = []; + foreach ($field as $f) { + $quoted[] = $this->_driver->quoteIdentifier($f); + } + $expression->setField($quoted); + } elseif ($field instanceof ExpressionInterface) { + $this->quoteExpression($field); + } + } + + /** + * Quotes identifiers in "order by" expression objects + * + * Strings with spaces are treated as literal expressions + * and will not have identifiers quoted. + * + * @param \Cake\Database\Expression\OrderByExpression $expression The expression to quote. + * @return void + */ + protected function _quoteOrderBy(OrderByExpression $expression) + { + $expression->iterateParts(function ($part, &$field) { + if (is_string($field)) { + $field = $this->_driver->quoteIdentifier($field); + + return $part; + } + if (is_string($part) && strpos($part, ' ') === false) { + return $this->_driver->quoteIdentifier($part); + } + + return $part; + }); + } + + /** + * Quotes identifiers in "order by" expression objects + * + * @param \Cake\Database\Expression\IdentifierExpression $expression The identifiers to quote. + * @return void + */ + protected function _quoteIdentifierExpression(IdentifierExpression $expression) + { + $expression->setIdentifier( + $this->_driver->quoteIdentifier($expression->getIdentifier()) + ); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Database/LICENSE.txt new file mode 100644 index 000000000..0c4b7932c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2016, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/vendor/cakephp/cakephp/src/Database/Log/LoggedQuery.php b/app/vendor/cakephp/cakephp/src/Database/Log/LoggedQuery.php new file mode 100644 index 000000000..90c1b100d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Log/LoggedQuery.php @@ -0,0 +1,70 @@ +took} rows={$this->numRows} {$this->query}"; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Log/LoggingStatement.php b/app/vendor/cakephp/cakephp/src/Database/Log/LoggingStatement.php new file mode 100644 index 000000000..525eb48e3 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Log/LoggingStatement.php @@ -0,0 +1,149 @@ +queryString = $this->queryString; + $query->error = $e; + $this->_log($query, $params, $t); + throw $e; + } + + $query->numRows = $this->rowCount(); + $this->_log($query, $params, $t); + + return $result; + } + + /** + * Copies the logging data to the passed LoggedQuery and sends it + * to the logging system. + * + * @param \Cake\Database\Log\LoggedQuery $query The query to log. + * @param array $params List of values to be bound to query. + * @param float $startTime The microtime when the query was executed. + * @return void + */ + protected function _log($query, $params, $startTime) + { + $query->took = round((microtime(true) - $startTime) * 1000, 0); + $query->params = $params ?: $this->_compiledParams; + $query->query = $this->queryString; + $this->getLogger()->log($query); + } + + /** + * Wrapper for bindValue function to gather each parameter to be later used + * in the logger function. + * + * @param string|int $column Name or param position to be bound + * @param mixed $value The value to bind to variable in query + * @param string|int|null $type PDO type or name of configured Type class + * @return void + */ + public function bindValue($column, $value, $type = 'string') + { + parent::bindValue($column, $value, $type); + if ($type === null) { + $type = 'string'; + } + if (!ctype_digit($type)) { + $value = $this->cast($value, $type)[0]; + } + $this->_compiledParams[$column] = $value; + } + + /** + * Sets the logger object instance. When called with no arguments + * it returns the currently setup logger instance + * + * @deprecated 3.5.0 Use getLogger() and setLogger() instead. + * @param \Cake\Database\Log\QueryLogger|null $instance Logger object instance. + * @return \Cake\Database\Log\QueryLogger|null Logger instance + */ + public function logger($instance = null) + { + deprecationWarning( + 'LoggingStatement::logger() is deprecated. ' . + 'Use LoggingStatement::setLogger()/getLogger() instead.' + ); + if ($instance === null) { + return $this->getLogger(); + } + + return $this->_logger = $instance; + } + + /** + * Sets a logger + * + * @param \Cake\Database\Log\QueryLogger $logger Logger object + * @return void + */ + public function setLogger($logger) + { + $this->_logger = $logger; + } + + /** + * Gets the logger object + * + * @return \Cake\Database\Log\QueryLogger logger instance + */ + public function getLogger() + { + return $this->_logger; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Log/QueryLogger.php b/app/vendor/cakephp/cakephp/src/Database/Log/QueryLogger.php new file mode 100644 index 000000000..c80669e93 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Log/QueryLogger.php @@ -0,0 +1,94 @@ +params)) { + $query->query = $this->_interpolate($query); + } + $this->_log($query); + } + + /** + * Wrapper function for the logger object, useful for unit testing + * or for overriding in subclasses. + * + * @param \Cake\Database\Log\LoggedQuery $query to be written in log + * @return void + */ + protected function _log($query) + { + Log::write('debug', $query, ['queriesLog']); + } + + /** + * Helper function used to replace query placeholders by the real + * params used to execute the query + * + * @param \Cake\Database\Log\LoggedQuery $query The query to log + * @return string + */ + protected function _interpolate($query) + { + $params = array_map(function ($p) { + if ($p === null) { + return 'NULL'; + } + if (is_bool($p)) { + return $p ? '1' : '0'; + } + + if (is_string($p)) { + $replacements = [ + '$' => '\\$', + '\\' => '\\\\\\\\', + "'" => "''", + ]; + + $p = strtr($p, $replacements); + + return "'$p'"; + } + + return $p; + }, $query->params); + + $keys = []; + $limit = is_int(key($params)) ? 1 : -1; + foreach ($params as $key => $param) { + $keys[] = is_string($key) ? "/:$key\b/" : '/[?]/'; + } + + return preg_replace($keys, $params, $query->query, $limit); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Query.php b/app/vendor/cakephp/cakephp/src/Database/Query.php new file mode 100644 index 000000000..50b48b911 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Query.php @@ -0,0 +1,2330 @@ + true, + 'update' => [], + 'set' => [], + 'insert' => [], + 'values' => [], + 'select' => [], + 'distinct' => false, + 'modifier' => [], + 'from' => [], + 'join' => [], + 'where' => null, + 'group' => [], + 'having' => null, + 'order' => null, + 'limit' => null, + 'offset' => null, + 'union' => [], + 'epilog' => null + ]; + + /** + * Indicates whether internal state of this query was changed, this is used to + * discard internal cached objects such as the transformed query or the reference + * to the executed statement. + * + * @var bool + */ + protected $_dirty = false; + + /** + * A list of callback functions to be called to alter each row from resulting + * statement upon retrieval. Each one of the callback function will receive + * the row array as first argument. + * + * @var array + */ + protected $_resultDecorators = []; + + /** + * Statement object resulting from executing this query. + * + * @var \Cake\Database\StatementInterface|null + */ + protected $_iterator; + + /** + * The object responsible for generating query placeholders and temporarily store values + * associated to each of those. + * + * @var \Cake\Database\ValueBinder|null + */ + protected $_valueBinder; + + /** + * Instance of functions builder object used for generating arbitrary SQL functions. + * + * @var \Cake\Database\FunctionsBuilder|null + */ + protected $_functionsBuilder; + + /** + * Boolean for tracking whether or not buffered results + * are enabled. + * + * @var bool + */ + protected $_useBufferedResults = true; + + /** + * The Type map for fields in the select clause + * + * @var \Cake\Database\TypeMap + */ + protected $_selectTypeMap; + + /** + * Tracking flag to disable casting + * + * @var bool + */ + protected $typeCastEnabled = true; + + /** + * Constructor. + * + * @param \Cake\Database\Connection $connection The connection + * object to be used for transforming and executing this query + */ + public function __construct($connection) + { + $this->setConnection($connection); + } + + /** + * Sets the connection instance to be used for executing and transforming this query. + * + * @param \Cake\Database\Connection $connection Connection instance + * @return $this + */ + public function setConnection($connection) + { + $this->_dirty(); + $this->_connection = $connection; + + return $this; + } + + /** + * Gets the connection instance to be used for executing and transforming this query. + * + * @return \Cake\Database\Connection + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * Sets the connection instance to be used for executing and transforming this query + * When called with a null argument, it will return the current connection instance. + * + * @deprecated 3.4.0 Use setConnection()/getConnection() instead. + * @param \Cake\Database\Connection|null $connection Connection instance + * @return $this|\Cake\Database\Connection + */ + public function connection($connection = null) + { + deprecationWarning( + 'Query::connection() is deprecated. ' . + 'Use Query::setConnection()/getConnection() instead.' + ); + if ($connection !== null) { + return $this->setConnection($connection); + } + + return $this->getConnection(); + } + + /** + * Compiles the SQL representation of this query and executes it using the + * configured connection object. Returns the resulting statement object. + * + * Executing a query internally executes several steps, the first one is + * letting the connection transform this object to fit its particular dialect, + * this might result in generating a different Query object that will be the one + * to actually be executed. Immediately after, literal values are passed to the + * connection so they are bound to the query in a safe way. Finally, the resulting + * statement is decorated with custom objects to execute callbacks for each row + * retrieved if necessary. + * + * Resulting statement is traversable, so it can be used in any loop as you would + * with an array. + * + * This method can be overridden in query subclasses to decorate behavior + * around query execution. + * + * @return \Cake\Database\StatementInterface + */ + public function execute() + { + $statement = $this->_connection->run($this); + $this->_iterator = $this->_decorateStatement($statement); + $this->_dirty = false; + + return $this->_iterator; + } + + /** + * Executes the SQL of this query and immediately closes the statement before returning the row count of records + * changed. + * + * This method can be used with UPDATE and DELETE queries, but is not recommended for SELECT queries and is not + * used to count records. + * + * ## Example + * + * ``` + * $rowCount = $query->update('articles') + * ->set(['published'=>true]) + * ->where(['published'=>false]) + * ->rowCountAndClose(); + * ``` + * + * The above example will change the published column to true for all false records, and return the number of + * records that were updated. + * + * @return int + */ + public function rowCountAndClose() + { + $statement = $this->execute(); + try { + return $statement->rowCount(); + } finally { + $statement->closeCursor(); + } + } + + /** + * Returns the SQL representation of this object. + * + * This function will compile this query to make it compatible + * with the SQL dialect that is used by the connection, This process might + * add, remove or alter any query part or internal expression to make it + * executable in the target platform. + * + * The resulting query may have placeholders that will be replaced with the actual + * values when the query is executed, hence it is most suitable to use with + * prepared statements. + * + * @param \Cake\Database\ValueBinder|null $generator A placeholder object that will hold + * associated values for expressions + * @return string + */ + public function sql(ValueBinder $generator = null) + { + if (!$generator) { + $generator = $this->getValueBinder(); + $generator->resetCount(); + } + + return $this->getConnection()->compileQuery($this, $generator); + } + + /** + * Will iterate over every specified part. Traversing functions can aggregate + * results using variables in the closure or instance variables. This function + * is commonly used as a way for traversing all query parts that + * are going to be used for constructing a query. + * + * The callback will receive 2 parameters, the first one is the value of the query + * part that is being iterated and the second the name of such part. + * + * ### Example: + * ``` + * $query->select(['title'])->from('articles')->traverse(function ($value, $clause) { + * if ($clause === 'select') { + * var_dump($value); + * } + * }, ['select', 'from']); + * ``` + * + * @param callable $visitor A function or callable to be executed for each part + * @param array $parts The query clauses to traverse + * @return $this + */ + public function traverse(callable $visitor, array $parts = []) + { + $parts = $parts ?: array_keys($this->_parts); + foreach ($parts as $name) { + $visitor($this->_parts[$name], $name); + } + + return $this; + } + + /** + * Adds new fields to be returned by a `SELECT` statement when this query is + * executed. Fields can be passed as an array of strings, array of expression + * objects, a single expression or a single string. + * + * If an array is passed, keys will be used to alias fields using the value as the + * real field to be aliased. It is possible to alias strings, Expression objects or + * even other Query objects. + * + * If a callable function is passed, the returning array of the function will + * be used as the list of fields. + * + * By default this function will append any passed argument to the list of fields + * to be selected, unless the second argument is set to true. + * + * ### Examples: + * + * ``` + * $query->select(['id', 'title']); // Produces SELECT id, title + * $query->select(['author' => 'author_id']); // Appends author: SELECT id, title, author_id as author + * $query->select('id', true); // Resets the list: SELECT id + * $query->select(['total' => $countQuery]); // SELECT id, (SELECT ...) AS total + * $query->select(function ($query) { + * return ['article_id', 'total' => $query->count('*')]; + * }) + * ``` + * + * By default no fields are selected, if you have an instance of `Cake\ORM\Query` and try to append + * fields you should also call `Cake\ORM\Query::enableAutoFields()` to select the default fields + * from the table. + * + * @param array|\Cake\Database\ExpressionInterface|string|callable $fields fields to be added to the list. + * @param bool $overwrite whether to reset fields with passed list or not + * @return $this + */ + public function select($fields = [], $overwrite = false) + { + if (!is_string($fields) && is_callable($fields)) { + $fields = $fields($this); + } + + if (!is_array($fields)) { + $fields = [$fields]; + } + + if ($overwrite) { + $this->_parts['select'] = $fields; + } else { + $this->_parts['select'] = array_merge($this->_parts['select'], $fields); + } + + $this->_dirty(); + $this->_type = 'select'; + + return $this; + } + + /** + * Adds a `DISTINCT` clause to the query to remove duplicates from the result set. + * This clause can only be used for select statements. + * + * If you wish to filter duplicates based of those rows sharing a particular field + * or set of fields, you may pass an array of fields to filter on. Beware that + * this option might not be fully supported in all database systems. + * + * ### Examples: + * + * ``` + * // Filters products with the same name and city + * $query->select(['name', 'city'])->from('products')->distinct(); + * + * // Filters products in the same city + * $query->distinct(['city']); + * $query->distinct('city'); + * + * // Filter products with the same name + * $query->distinct(['name'], true); + * $query->distinct('name', true); + * ``` + * + * @param array|\Cake\Database\ExpressionInterface|string|bool $on Enable/disable distinct class + * or list of fields to be filtered on + * @param bool $overwrite whether to reset fields with passed list or not + * @return $this + */ + public function distinct($on = [], $overwrite = false) + { + if ($on === []) { + $on = true; + } elseif (is_string($on)) { + $on = [$on]; + } + + if (is_array($on)) { + $merge = []; + if (is_array($this->_parts['distinct'])) { + $merge = $this->_parts['distinct']; + } + $on = $overwrite ? array_values($on) : array_merge($merge, array_values($on)); + } + + $this->_parts['distinct'] = $on; + $this->_dirty(); + + return $this; + } + + /** + * Adds a single or multiple `SELECT` modifiers to be used in the `SELECT`. + * + * By default this function will append any passed argument to the list of modifiers + * to be applied, unless the second argument is set to true. + * + * ### Example: + * + * ``` + * // Ignore cache query in MySQL + * $query->select(['name', 'city'])->from('products')->modifier('SQL_NO_CACHE'); + * // It will produce the SQL: SELECT SQL_NO_CACHE name, city FROM products + * + * // Or with multiple modifiers + * $query->select(['name', 'city'])->from('products')->modifier(['HIGH_PRIORITY', 'SQL_NO_CACHE']); + * // It will produce the SQL: SELECT HIGH_PRIORITY SQL_NO_CACHE name, city FROM products + * ``` + * + * @param array|\Cake\Database\ExpressionInterface|string $modifiers modifiers to be applied to the query + * @param bool $overwrite whether to reset order with field list or not + * @return $this + */ + public function modifier($modifiers, $overwrite = false) + { + $this->_dirty(); + if ($overwrite) { + $this->_parts['modifier'] = []; + } + $this->_parts['modifier'] = array_merge($this->_parts['modifier'], (array)$modifiers); + + return $this; + } + + /** + * Adds a single or multiple tables to be used in the FROM clause for this query. + * Tables can be passed as an array of strings, array of expression + * objects, a single expression or a single string. + * + * If an array is passed, keys will be used to alias tables using the value as the + * real field to be aliased. It is possible to alias strings, ExpressionInterface objects or + * even other Query objects. + * + * By default this function will append any passed argument to the list of tables + * to be selected from, unless the second argument is set to true. + * + * This method can be used for select, update and delete statements. + * + * ### Examples: + * + * ``` + * $query->from(['p' => 'posts']); // Produces FROM posts p + * $query->from('authors'); // Appends authors: FROM posts p, authors + * $query->from(['products'], true); // Resets the list: FROM products + * $query->from(['sub' => $countQuery]); // FROM (SELECT ...) sub + * ``` + * + * @param array|string $tables tables to be added to the list. This argument, can be + * passed as an array of strings, array of expression objects, or a single string. See + * the examples above for the valid call types. + * @param bool $overwrite whether to reset tables with passed list or not + * @return $this|array + */ + public function from($tables = [], $overwrite = false) + { + if (empty($tables)) { + return $this->_parts['from']; + } + + $tables = (array)$tables; + + if ($overwrite) { + $this->_parts['from'] = $tables; + } else { + $this->_parts['from'] = array_merge($this->_parts['from'], $tables); + } + + $this->_dirty(); + + return $this; + } + + /** + * Adds a single or multiple tables to be used as JOIN clauses to this query. + * Tables can be passed as an array of strings, an array describing the + * join parts, an array with multiple join descriptions, or a single string. + * + * By default this function will append any passed argument to the list of tables + * to be joined, unless the third argument is set to true. + * + * When no join type is specified an `INNER JOIN` is used by default: + * `$query->join(['authors'])` will produce `INNER JOIN authors ON 1 = 1` + * + * It is also possible to alias joins using the array key: + * `$query->join(['a' => 'authors'])` will produce `INNER JOIN authors a ON 1 = 1` + * + * A join can be fully described and aliased using the array notation: + * + * ``` + * $query->join([ + * 'a' => [ + * 'table' => 'authors', + * 'type' => 'LEFT', + * 'conditions' => 'a.id = b.author_id' + * ] + * ]); + * // Produces LEFT JOIN authors a ON a.id = b.author_id + * ``` + * + * You can even specify multiple joins in an array, including the full description: + * + * ``` + * $query->join([ + * 'a' => [ + * 'table' => 'authors', + * 'type' => 'LEFT', + * 'conditions' => 'a.id = b.author_id' + * ], + * 'p' => [ + * 'table' => 'publishers', + * 'type' => 'INNER', + * 'conditions' => 'p.id = b.publisher_id AND p.name = "Cake Software Foundation"' + * ] + * ]); + * // LEFT JOIN authors a ON a.id = b.author_id + * // INNER JOIN publishers p ON p.id = b.publisher_id AND p.name = "Cake Software Foundation" + * ``` + * + * ### Using conditions and types + * + * Conditions can be expressed, as in the examples above, using a string for comparing + * columns, or string with already quoted literal values. Additionally it is + * possible to use conditions expressed in arrays or expression objects. + * + * When using arrays for expressing conditions, it is often desirable to convert + * the literal values to the correct database representation. This is achieved + * using the second parameter of this function. + * + * ``` + * $query->join(['a' => [ + * 'table' => 'articles', + * 'conditions' => [ + * 'a.posted >=' => new DateTime('-3 days'), + * 'a.published' => true, + * 'a.author_id = authors.id' + * ] + * ]], ['a.posted' => 'datetime', 'a.published' => 'boolean']) + * ``` + * + * ### Overwriting joins + * + * When creating aliased joins using the array notation, you can override + * previous join definitions by using the same alias in consequent + * calls to this function or you can replace all previously defined joins + * with another list if the third parameter for this function is set to true. + * + * ``` + * $query->join(['alias' => 'table']); // joins table with as alias + * $query->join(['alias' => 'another_table']); // joins another_table with as alias + * $query->join(['something' => 'different_table'], [], true); // resets joins list + * ``` + * + * @param array|string|null $tables list of tables to be joined in the query + * @param array $types associative array of type names used to bind values to query + * @param bool $overwrite whether to reset joins with passed list or not + * @see \Cake\Database\Type + * @return $this|array + */ + public function join($tables = null, $types = [], $overwrite = false) + { + if ($tables === null) { + return $this->_parts['join']; + } + + if (is_string($tables) || isset($tables['table'])) { + $tables = [$tables]; + } + + $joins = []; + $i = count($this->_parts['join']); + foreach ($tables as $alias => $t) { + if (!is_array($t)) { + $t = ['table' => $t, 'conditions' => $this->newExpr()]; + } + + if (!is_string($t['conditions']) && is_callable($t['conditions'])) { + $t['conditions'] = $t['conditions']($this->newExpr(), $this); + } + + if (!($t['conditions'] instanceof ExpressionInterface)) { + $t['conditions'] = $this->newExpr()->add($t['conditions'], $types); + } + $alias = is_string($alias) ? $alias : null; + $joins[$alias ?: $i++] = $t + ['type' => QueryInterface::JOIN_TYPE_INNER, 'alias' => $alias]; + } + + if ($overwrite) { + $this->_parts['join'] = $joins; + } else { + $this->_parts['join'] = array_merge($this->_parts['join'], $joins); + } + + $this->_dirty(); + + return $this; + } + + /** + * Remove a join if it has been defined. + * + * Useful when you are redefining joins or want to re-order + * the join clauses. + * + * @param string $name The alias/name of the join to remove. + * @return $this + */ + public function removeJoin($name) + { + unset($this->_parts['join'][$name]); + $this->_dirty(); + + return $this; + } + + /** + * Adds a single `LEFT JOIN` clause to the query. + * + * This is a shorthand method for building joins via `join()`. + * + * The table name can be passed as a string, or as an array in case it needs to + * be aliased: + * + * ``` + * // LEFT JOIN authors ON authors.id = posts.author_id + * $query->leftJoin('authors', 'authors.id = posts.author_id'); + * + * // LEFT JOIN authors a ON a.id = posts.author_id + * $query->leftJoin(['a' => 'authors'], 'a.id = posts.author_id'); + * ``` + * + * Conditions can be passed as strings, arrays, or expression objects. When + * using arrays it is possible to combine them with the `$types` parameter + * in order to define how to convert the values: + * + * ``` + * $query->leftJoin(['a' => 'articles'], [ + * 'a.posted >=' => new DateTime('-3 days'), + * 'a.published' => true, + * 'a.author_id = authors.id' + * ], ['a.posted' => 'datetime', 'a.published' => 'boolean']); + * ``` + * + * See `join()` for further details on conditions and types. + * + * @param string|array $table The table to join with + * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions + * to use for joining. + * @param array $types a list of types associated to the conditions used for converting + * values to the corresponding database representation. + * @return $this + */ + public function leftJoin($table, $conditions = [], $types = []) + { + return $this->join($this->_makeJoin($table, $conditions, QueryInterface::JOIN_TYPE_LEFT), $types); + } + + /** + * Adds a single `RIGHT JOIN` clause to the query. + * + * This is a shorthand method for building joins via `join()`. + * + * The arguments of this method are identical to the `leftJoin()` shorthand, please refer + * to that methods description for further details. + * + * @param string|array $table The table to join with + * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions + * to use for joining. + * @param array $types a list of types associated to the conditions used for converting + * values to the corresponding database representation. + * @return $this + */ + public function rightJoin($table, $conditions = [], $types = []) + { + return $this->join($this->_makeJoin($table, $conditions, QueryInterface::JOIN_TYPE_RIGHT), $types); + } + + /** + * Adds a single `INNER JOIN` clause to the query. + * + * This is a shorthand method for building joins via `join()`. + * + * The arguments of this method are identical to the `leftJoin()` shorthand, please refer + * to that methods description for further details. + * + * @param string|array $table The table to join with + * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions + * to use for joining. + * @param array $types a list of types associated to the conditions used for converting + * values to the corresponding database representation. + * @return $this + */ + public function innerJoin($table, $conditions = [], $types = []) + { + return $this->join($this->_makeJoin($table, $conditions, QueryInterface::JOIN_TYPE_INNER), $types); + } + + /** + * Returns an array that can be passed to the join method describing a single join clause + * + * @param string|array $table The table to join with + * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions + * to use for joining. + * @param string $type the join type to use + * @return array + */ + protected function _makeJoin($table, $conditions, $type) + { + $alias = $table; + + if (is_array($table)) { + $alias = key($table); + $table = current($table); + } + + return [ + $alias => [ + 'table' => $table, + 'conditions' => $conditions, + 'type' => $type + ] + ]; + } + + /** + * Adds a condition or set of conditions to be used in the WHERE clause for this + * query. Conditions can be expressed as an array of fields as keys with + * comparison operators in it, the values for the array will be used for comparing + * the field to such literal. Finally, conditions can be expressed as a single + * string or an array of strings. + * + * When using arrays, each entry will be joined to the rest of the conditions using + * an `AND` operator. Consecutive calls to this function will also join the new + * conditions specified using the AND operator. Additionally, values can be + * expressed using expression objects which can include other query objects. + * + * Any conditions created with this methods can be used with any `SELECT`, `UPDATE` + * and `DELETE` type of queries. + * + * ### Conditions using operators: + * + * ``` + * $query->where([ + * 'posted >=' => new DateTime('3 days ago'), + * 'title LIKE' => 'Hello W%', + * 'author_id' => 1, + * ], ['posted' => 'datetime']); + * ``` + * + * The previous example produces: + * + * `WHERE posted >= 2012-01-27 AND title LIKE 'Hello W%' AND author_id = 1` + * + * Second parameter is used to specify what type is expected for each passed + * key. Valid types can be used from the mapped with Database\Type class. + * + * ### Nesting conditions with conjunctions: + * + * ``` + * $query->where([ + * 'author_id !=' => 1, + * 'OR' => ['published' => true, 'posted <' => new DateTime('now')], + * 'NOT' => ['title' => 'Hello'] + * ], ['published' => boolean, 'posted' => 'datetime'] + * ``` + * + * The previous example produces: + * + * `WHERE author_id = 1 AND (published = 1 OR posted < '2012-02-01') AND NOT (title = 'Hello')` + * + * You can nest conditions using conjunctions as much as you like. Sometimes, you + * may want to define 2 different options for the same key, in that case, you can + * wrap each condition inside a new array: + * + * `$query->where(['OR' => [['published' => false], ['published' => true]])` + * + * Would result in: + * + * `WHERE (published = false) OR (published = true)` + * + * Keep in mind that every time you call where() with the third param set to false + * (default), it will join the passed conditions to the previous stored list using + * the `AND` operator. Also, using the same array key twice in consecutive calls to + * this method will not override the previous value. + * + * ### Using expressions objects: + * + * ``` + * $exp = $query->newExpr()->add(['id !=' => 100, 'author_id' != 1])->tieWith('OR'); + * $query->where(['published' => true], ['published' => 'boolean'])->where($exp); + * ``` + * + * The previous example produces: + * + * `WHERE (id != 100 OR author_id != 1) AND published = 1` + * + * Other Query objects that be used as conditions for any field. + * + * ### Adding conditions in multiple steps: + * + * You can use callable functions to construct complex expressions, functions + * receive as first argument a new QueryExpression object and this query instance + * as second argument. Functions must return an expression object, that will be + * added the list of conditions for the query using the `AND` operator. + * + * ``` + * $query + * ->where(['title !=' => 'Hello World']) + * ->where(function ($exp, $query) { + * $or = $exp->or_(['id' => 1]); + * $and = $exp->and_(['id >' => 2, 'id <' => 10]); + * return $or->add($and); + * }); + * ``` + * + * * The previous example produces: + * + * `WHERE title != 'Hello World' AND (id = 1 OR (id > 2 AND id < 10))` + * + * ### Conditions as strings: + * + * ``` + * $query->where(['articles.author_id = authors.id', 'modified IS NULL']); + * ``` + * + * The previous example produces: + * + * `WHERE articles.author_id = authors.id AND modified IS NULL` + * + * Please note that when using the array notation or the expression objects, all + * *values* will be correctly quoted and transformed to the correspondent database + * data type automatically for you, thus securing your application from SQL injections. + * The keys however, are not treated as unsafe input, and should be sanitized/whitelisted. + * + * If you use string conditions make sure that your values are correctly quoted. + * The safest thing you can do is to never use string conditions. + * + * @param string|array|\Cake\Database\ExpressionInterface|callable|null $conditions The conditions to filter on. + * @param array $types associative array of type names used to bind values to query + * @param bool $overwrite whether to reset conditions with passed list or not + * @see \Cake\Database\Type + * @see \Cake\Database\Expression\QueryExpression + * @return $this + */ + public function where($conditions = null, $types = [], $overwrite = false) + { + if ($overwrite) { + $this->_parts['where'] = $this->newExpr(); + } + $this->_conjugate('where', $conditions, 'AND', $types); + + return $this; + } + + /** + * Convenience method that adds a NOT NULL condition to the query + * + * @param array|string|\Cake\Database\ExpressionInterface $fields A single field or expressions or a list of them that should be not null + * @return $this + */ + public function whereNotNull($fields) + { + if (!is_array($fields)) { + $fields = [$fields]; + } + + $exp = $this->newExpr(); + + foreach ($fields as $field) { + $exp->isNotNull($field); + } + + return $this->where($exp); + } + + /** + * Convenience method that adds a IS NULL condition to the query + * + * @param array|string|\Cake\Database\ExpressionInterface $fields A single field or expressions or a list of them that should be null + * @return $this + */ + public function whereNull($fields) + { + if (!is_array($fields)) { + $fields = [$fields]; + } + + $exp = $this->newExpr(); + + foreach ($fields as $field) { + $exp->isNull($field); + } + + return $this->where($exp); + } + + /** + * Adds an IN condition or set of conditions to be used in the WHERE clause for this + * query. + * + * This method does allow empty inputs in contrast to where() if you set + * 'allowEmpty' to true. + * Be careful about using it without proper sanity checks. + * + * Options: + * - `types` - Associative array of type names used to bind values to query + * - `allowEmpty` - Allow empty array. + * + * @param string $field Field + * @param array $values Array of values + * @param array $options Options + * @return $this + */ + public function whereInList($field, array $values, array $options = []) + { + $options += [ + 'types' => [], + 'allowEmpty' => false, + ]; + + if ($options['allowEmpty'] && !$values) { + return $this->where('1=0'); + } + + return $this->where([$field . ' IN' => $values], $options['types']); + } + + /** + * Adds a NOT IN condition or set of conditions to be used in the WHERE clause for this + * query. + * + * This method does allow empty inputs in contrast to where() if you set + * 'allowEmpty' to true. + * Be careful about using it without proper sanity checks. + * + * @param string $field Field + * @param array $values Array of values + * @param array $options Options + * @return $this + */ + public function whereNotInList($field, array $values, array $options = []) + { + $options += [ + 'types' => [], + 'allowEmpty' => false, + ]; + + if ($options['allowEmpty'] && !$values) { + return $this->where([$field . ' IS NOT' => null]); + } + + return $this->where([$field . ' NOT IN' => $values], $options['types']); + } + + /** + * Connects any previously defined set of conditions to the provided list + * using the AND operator. This function accepts the conditions list in the same + * format as the method `where` does, hence you can use arrays, expression objects + * callback functions or strings. + * + * It is important to notice that when calling this function, any previous set + * of conditions defined for this query will be treated as a single argument for + * the AND operator. This function will not only operate the most recently defined + * condition, but all the conditions as a whole. + * + * When using an array for defining conditions, creating constraints form each + * array entry will use the same logic as with the `where()` function. This means + * that each array entry will be joined to the other using the AND operator, unless + * you nest the conditions in the array using other operator. + * + * ### Examples: + * + * ``` + * $query->where(['title' => 'Hello World')->andWhere(['author_id' => 1]); + * ``` + * + * Will produce: + * + * `WHERE title = 'Hello World' AND author_id = 1` + * + * ``` + * $query + * ->where(['OR' => ['published' => false, 'published is NULL']]) + * ->andWhere(['author_id' => 1, 'comments_count >' => 10]) + * ``` + * + * Produces: + * + * `WHERE (published = 0 OR published IS NULL) AND author_id = 1 AND comments_count > 10` + * + * ``` + * $query + * ->where(['title' => 'Foo']) + * ->andWhere(function ($exp, $query) { + * return $exp + * ->or_(['author_id' => 1]) + * ->add(['author_id' => 2]); + * }); + * ``` + * + * Generates the following conditions: + * + * `WHERE (title = 'Foo') AND (author_id = 1 OR author_id = 2)` + * + * @param string|array|\Cake\Database\ExpressionInterface|callable $conditions The conditions to add with AND. + * @param array $types associative array of type names used to bind values to query + * @see \Cake\Database\Query::where() + * @see \Cake\Database\Type + * @return $this + */ + public function andWhere($conditions, $types = []) + { + $this->_conjugate('where', $conditions, 'AND', $types); + + return $this; + } + + /** + * Connects any previously defined set of conditions to the provided list + * using the OR operator. This function accepts the conditions list in the same + * format as the method `where` does, hence you can use arrays, expression objects + * callback functions or strings. + * + * It is important to notice that when calling this function, any previous set + * of conditions defined for this query will be treated as a single argument for + * the OR operator. This function will not only operate the most recently defined + * condition, but all the conditions as a whole. + * + * When using an array for defining conditions, creating constraints form each + * array entry will use the same logic as with the `where()` function. This means + * that each array entry will be joined to the other using the OR operator, unless + * you nest the conditions in the array using other operator. + * + * ### Examples: + * + * ``` + * $query->where(['title' => 'Hello World')->orWhere(['title' => 'Foo']); + * ``` + * + * Will produce: + * + * `WHERE title = 'Hello World' OR title = 'Foo'` + * + * ``` + * $query + * ->where(['OR' => ['published' => false, 'published is NULL']]) + * ->orWhere(['author_id' => 1, 'comments_count >' => 10]) + * ``` + * + * Produces: + * + * `WHERE (published = 0 OR published IS NULL) OR (author_id = 1 AND comments_count > 10)` + * + * ``` + * $query + * ->where(['title' => 'Foo']) + * ->orWhere(function ($exp, $query) { + * return $exp + * ->or_(['author_id' => 1]) + * ->add(['author_id' => 2]); + * }); + * ``` + * + * Generates the following conditions: + * + * `WHERE (title = 'Foo') OR (author_id = 1 OR author_id = 2)` + * + * @param string|array|\Cake\Database\ExpressionInterface|callable $conditions The conditions to add with OR. + * @param array $types associative array of type names used to bind values to query + * @see \Cake\Database\Query::where() + * @see \Cake\Database\Type + * @return $this + * @deprecated 3.5.0 This method creates hard to predict SQL based on the current query state. + * Use `Query::where()` instead as it has more predicatable and easier to understand behavior. + */ + public function orWhere($conditions, $types = []) + { + deprecationWarning( + 'Query::orWhere() is deprecated as it creates hard to predict SQL based on the ' . + 'current query state. Use `Query::where()` instead.' + ); + $this->_conjugate('where', $conditions, 'OR', $types); + + return $this; + } + + /** + * Adds a single or multiple fields to be used in the ORDER clause for this query. + * Fields can be passed as an array of strings, array of expression + * objects, a single expression or a single string. + * + * If an array is passed, keys will be used as the field itself and the value will + * represent the order in which such field should be ordered. When called multiple + * times with the same fields as key, the last order definition will prevail over + * the others. + * + * By default this function will append any passed argument to the list of fields + * to be selected, unless the second argument is set to true. + * + * ### Examples: + * + * ``` + * $query->order(['title' => 'DESC', 'author_id' => 'ASC']); + * ``` + * + * Produces: + * + * `ORDER BY title DESC, author_id ASC` + * + * ``` + * $query->order(['title' => 'DESC NULLS FIRST'])->order('author_id'); + * ``` + * + * Will generate: + * + * `ORDER BY title DESC NULLS FIRST, author_id` + * + * ``` + * $expression = $query->newExpr()->add(['id % 2 = 0']); + * $query->order($expression)->order(['title' => 'ASC']); + * ``` + * + * Will become: + * + * `ORDER BY (id %2 = 0), title ASC` + * + * Order fields/directions are not sanitized by the query builder. + * You should use a whitelist of fields/directions when passing + * in user-supplied data to `order()`. + * + * If you need to set complex expressions as order conditions, you + * should use `orderAsc()` or `orderDesc()`. + * + * @param array|\Cake\Database\ExpressionInterface|string $fields fields to be added to the list + * @param bool $overwrite whether to reset order with field list or not + * @return $this + */ + public function order($fields, $overwrite = false) + { + if ($overwrite) { + $this->_parts['order'] = null; + } + + if (!$fields) { + return $this; + } + + if (!$this->_parts['order']) { + $this->_parts['order'] = new OrderByExpression(); + } + $this->_conjugate('order', $fields, '', []); + + return $this; + } + + /** + * Add an ORDER BY clause with an ASC direction. + * + * This method allows you to set complex expressions + * as order conditions unlike order() + * + * Order fields are not suitable for use with user supplied data as they are + * not sanitized by the query builder. + * + * @param string|\Cake\Database\Expression\QueryExpression $field The field to order on. + * @param bool $overwrite Whether or not to reset the order clauses. + * @return $this + */ + public function orderAsc($field, $overwrite = false) + { + if ($overwrite) { + $this->_parts['order'] = null; + } + if (!$field) { + return $this; + } + + if (!$this->_parts['order']) { + $this->_parts['order'] = new OrderByExpression(); + } + $this->_parts['order']->add(new OrderClauseExpression($field, 'ASC')); + + return $this; + } + + /** + * Add an ORDER BY clause with a DESC direction. + * + * This method allows you to set complex expressions + * as order conditions unlike order() + * + * Order fields are not suitable for use with user supplied data as they are + * not sanitized by the query builder. + * + * @param string|\Cake\Database\Expression\QueryExpression $field The field to order on. + * @param bool $overwrite Whether or not to reset the order clauses. + * @return $this + */ + public function orderDesc($field, $overwrite = false) + { + if ($overwrite) { + $this->_parts['order'] = null; + } + if (!$field) { + return $this; + } + + if (!$this->_parts['order']) { + $this->_parts['order'] = new OrderByExpression(); + } + $this->_parts['order']->add(new OrderClauseExpression($field, 'DESC')); + + return $this; + } + + /** + * Adds a single or multiple fields to be used in the GROUP BY clause for this query. + * Fields can be passed as an array of strings, array of expression + * objects, a single expression or a single string. + * + * By default this function will append any passed argument to the list of fields + * to be grouped, unless the second argument is set to true. + * + * ### Examples: + * + * ``` + * // Produces GROUP BY id, title + * $query->group(['id', 'title']); + * + * // Produces GROUP BY title + * $query->group('title'); + * ``` + * + * Group fields are not suitable for use with user supplied data as they are + * not sanitized by the query builder. + * + * @param array|\Cake\Database\ExpressionInterface|string $fields fields to be added to the list + * @param bool $overwrite whether to reset fields with passed list or not + * @return $this + */ + public function group($fields, $overwrite = false) + { + if ($overwrite) { + $this->_parts['group'] = []; + } + + if (!is_array($fields)) { + $fields = [$fields]; + } + + $this->_parts['group'] = array_merge($this->_parts['group'], array_values($fields)); + $this->_dirty(); + + return $this; + } + + /** + * Adds a condition or set of conditions to be used in the `HAVING` clause for this + * query. This method operates in exactly the same way as the method `where()` + * does. Please refer to its documentation for an insight on how to using each + * parameter. + * + * Having fields are not suitable for use with user supplied data as they are + * not sanitized by the query builder. + * + * @param string|array|\Cake\Database\ExpressionInterface|callable|null $conditions The having conditions. + * @param array $types associative array of type names used to bind values to query + * @param bool $overwrite whether to reset conditions with passed list or not + * @see \Cake\Database\Query::where() + * @return $this + */ + public function having($conditions = null, $types = [], $overwrite = false) + { + if ($overwrite) { + $this->_parts['having'] = $this->newExpr(); + } + $this->_conjugate('having', $conditions, 'AND', $types); + + return $this; + } + + /** + * Connects any previously defined set of conditions to the provided list + * using the AND operator in the HAVING clause. This method operates in exactly + * the same way as the method `andWhere()` does. Please refer to its + * documentation for an insight on how to using each parameter. + * + * Having fields are not suitable for use with user supplied data as they are + * not sanitized by the query builder. + * + * @param string|array|\Cake\Database\ExpressionInterface|callable $conditions The AND conditions for HAVING. + * @param array $types associative array of type names used to bind values to query + * @see \Cake\Database\Query::andWhere() + * @return $this + */ + public function andHaving($conditions, $types = []) + { + $this->_conjugate('having', $conditions, 'AND', $types); + + return $this; + } + + /** + * Connects any previously defined set of conditions to the provided list + * using the OR operator in the HAVING clause. This method operates in exactly + * the same way as the method `orWhere()` does. Please refer to its + * documentation for an insight on how to using each parameter. + * + * Having fields are not suitable for use with user supplied data as they are + * not sanitized by the query builder. + * + * @param string|array|\Cake\Database\ExpressionInterface|callable $conditions The OR conditions for HAVING. + * @param array $types associative array of type names used to bind values to query. + * @see \Cake\Database\Query::orWhere() + * @return $this + * @deprecated 3.5.0 This method creates hard to predict SQL based on the current query state. + * Use `Query::having()` instead as it has more predicatable and easier to understand behavior. + */ + public function orHaving($conditions, $types = []) + { + deprecationWarning('Query::orHaving() is deprecated. Use Query::having() instead.'); + $this->_conjugate('having', $conditions, 'OR', $types); + + return $this; + } + + /** + * Set the page of results you want. + * + * This method provides an easier to use interface to set the limit + offset + * in the record set you want as results. If empty the limit will default to + * the existing limit clause, and if that too is empty, then `25` will be used. + * + * Pages must start at 1. + * + * @param int $num The page number you want. + * @param int|null $limit The number of rows you want in the page. If null + * the current limit clause will be used. + * @return $this + * @throws \InvalidArgumentException If page number < 1. + */ + public function page($num, $limit = null) + { + if ($num < 1) { + throw new InvalidArgumentException('Pages must start at 1.'); + } + if ($limit !== null) { + $this->limit($limit); + } + $limit = $this->clause('limit'); + if ($limit === null) { + $limit = 25; + $this->limit($limit); + } + $offset = ($num - 1) * $limit; + if (PHP_INT_MAX <= $offset) { + $offset = PHP_INT_MAX; + } + $this->offset((int)$offset); + + return $this; + } + + /** + * Sets the number of records that should be retrieved from database, + * accepts an integer or an expression object that evaluates to an integer. + * In some databases, this operation might not be supported or will require + * the query to be transformed in order to limit the result set size. + * + * ### Examples + * + * ``` + * $query->limit(10) // generates LIMIT 10 + * $query->limit($query->newExpr()->add(['1 + 1'])); // LIMIT (1 + 1) + * ``` + * + * @param int|\Cake\Database\ExpressionInterface $num number of records to be returned + * @return $this + */ + public function limit($num) + { + $this->_dirty(); + if ($num !== null && !is_object($num)) { + $num = (int)$num; + } + $this->_parts['limit'] = $num; + + return $this; + } + + /** + * Sets the number of records that should be skipped from the original result set + * This is commonly used for paginating large results. Accepts an integer or an + * expression object that evaluates to an integer. + * + * In some databases, this operation might not be supported or will require + * the query to be transformed in order to limit the result set size. + * + * ### Examples + * + * ``` + * $query->offset(10) // generates OFFSET 10 + * $query->offset($query->newExpr()->add(['1 + 1'])); // OFFSET (1 + 1) + * ``` + * + * @param int|\Cake\Database\ExpressionInterface $num number of records to be skipped + * @return $this + */ + public function offset($num) + { + $this->_dirty(); + if ($num !== null && !is_object($num)) { + $num = (int)$num; + } + $this->_parts['offset'] = $num; + + return $this; + } + + /** + * Adds a complete query to be used in conjunction with an UNION operator with + * this query. This is used to combine the result set of this query with the one + * that will be returned by the passed query. You can add as many queries as you + * required by calling multiple times this method with different queries. + * + * By default, the UNION operator will remove duplicate rows, if you wish to include + * every row for all queries, use unionAll(). + * + * ### Examples + * + * ``` + * $union = (new Query($conn))->select(['id', 'title'])->from(['a' => 'articles']); + * $query->select(['id', 'name'])->from(['d' => 'things'])->union($union); + * ``` + * + * Will produce: + * + * `SELECT id, name FROM things d UNION SELECT id, title FROM articles a` + * + * @param string|\Cake\Database\Query $query full SQL query to be used in UNION operator + * @param bool $overwrite whether to reset the list of queries to be operated or not + * @return $this + */ + public function union($query, $overwrite = false) + { + if ($overwrite) { + $this->_parts['union'] = []; + } + $this->_parts['union'][] = [ + 'all' => false, + 'query' => $query + ]; + $this->_dirty(); + + return $this; + } + + /** + * Adds a complete query to be used in conjunction with the UNION ALL operator with + * this query. This is used to combine the result set of this query with the one + * that will be returned by the passed query. You can add as many queries as you + * required by calling multiple times this method with different queries. + * + * Unlike UNION, UNION ALL will not remove duplicate rows. + * + * ``` + * $union = (new Query($conn))->select(['id', 'title'])->from(['a' => 'articles']); + * $query->select(['id', 'name'])->from(['d' => 'things'])->unionAll($union); + * ``` + * + * Will produce: + * + * `SELECT id, name FROM things d UNION ALL SELECT id, title FROM articles a` + * + * @param string|\Cake\Database\Query $query full SQL query to be used in UNION operator + * @param bool $overwrite whether to reset the list of queries to be operated or not + * @return $this + */ + public function unionAll($query, $overwrite = false) + { + if ($overwrite) { + $this->_parts['union'] = []; + } + $this->_parts['union'][] = [ + 'all' => true, + 'query' => $query + ]; + $this->_dirty(); + + return $this; + } + + /** + * Create an insert query. + * + * Note calling this method will reset any data previously set + * with Query::values(). + * + * @param array $columns The columns to insert into. + * @param array $types A map between columns & their datatypes. + * @return $this + * @throws \RuntimeException When there are 0 columns. + */ + public function insert(array $columns, array $types = []) + { + if (empty($columns)) { + throw new RuntimeException('At least 1 column is required to perform an insert.'); + } + $this->_dirty(); + $this->_type = 'insert'; + $this->_parts['insert'][1] = $columns; + if (!$this->_parts['values']) { + $this->_parts['values'] = new ValuesExpression($columns, $this->getTypeMap()->setTypes($types)); + } else { + $this->_parts['values']->setColumns($columns); + } + + return $this; + } + + /** + * Set the table name for insert queries. + * + * @param string $table The table name to insert into. + * @return $this + */ + public function into($table) + { + $this->_dirty(); + $this->_type = 'insert'; + $this->_parts['insert'][0] = $table; + + return $this; + } + + /** + * Creates an expression that refers to an identifier. Identifiers are used to refer to field names and allow + * the SQL compiler to apply quotes or escape the identifier. + * + * The value is used as is, and you might be required to use aliases or include the table reference in + * the identifier. Do not use this method to inject SQL methods or logical statements. + * + * ### Example + * + * ``` + * $query->newExp()->lte('count', $query->identifier('total')); + * ``` + * + * @param string $identifier The identifier for an expression + * @return \Cake\Database\ExpressionInterface + */ + public function identifier($identifier) + { + return new IdentifierExpression($identifier); + } + + /** + * Set the values for an insert query. + * + * Multi inserts can be performed by calling values() more than one time, + * or by providing an array of value sets. Additionally $data can be a Query + * instance to insert data from another SELECT statement. + * + * @param array|\Cake\Database\Query $data The data to insert. + * @return $this + * @throws \Cake\Database\Exception if you try to set values before declaring columns. + * Or if you try to set values on non-insert queries. + */ + public function values($data) + { + if ($this->_type !== 'insert') { + throw new Exception( + 'You cannot add values before defining columns to use.' + ); + } + if (empty($this->_parts['insert'])) { + throw new Exception( + 'You cannot add values before defining columns to use.' + ); + } + + $this->_dirty(); + if ($data instanceof ValuesExpression) { + $this->_parts['values'] = $data; + + return $this; + } + + $this->_parts['values']->add($data); + + return $this; + } + + /** + * Create an update query. + * + * Can be combined with set() and where() methods to create update queries. + * + * @param string|\Cake\Database\ExpressionInterface $table The table you want to update. + * @return $this + */ + public function update($table) + { + if (!is_string($table) && !($table instanceof ExpressionInterface)) { + $text = 'Table must be of type string or "%s", got "%s"'; + $message = sprintf($text, ExpressionInterface::class, gettype($table)); + throw new InvalidArgumentException($message); + } + + $this->_dirty(); + $this->_type = 'update'; + $this->_parts['update'][0] = $table; + + return $this; + } + + /** + * Set one or many fields to update. + * + * ### Examples + * + * Passing a string: + * + * ``` + * $query->update('articles')->set('title', 'The Title'); + * ``` + * + * Passing an array: + * + * ``` + * $query->update('articles')->set(['title' => 'The Title'], ['title' => 'string']); + * ``` + * + * Passing a callable: + * + * ``` + * $query->update('articles')->set(function ($exp) { + * return $exp->eq('title', 'The title', 'string'); + * }); + * ``` + * + * @param string|array|callable|\Cake\Database\Expression\QueryExpression $key The column name or array of keys + * + values to set. This can also be a QueryExpression containing a SQL fragment. + * It can also be a callable, that is required to return an expression object. + * @param mixed $value The value to update $key to. Can be null if $key is an + * array or QueryExpression. When $key is an array, this parameter will be + * used as $types instead. + * @param array $types The column types to treat data as. + * @return $this + */ + public function set($key, $value = null, $types = []) + { + if (empty($this->_parts['set'])) { + $this->_parts['set'] = $this->newExpr()->setConjunction(','); + } + + if ($this->_parts['set']->isCallable($key)) { + $exp = $this->newExpr()->setConjunction(','); + $this->_parts['set']->add($key($exp)); + + return $this; + } + + if (is_array($key) || $key instanceof ExpressionInterface) { + $types = (array)$value; + $this->_parts['set']->add($key, $types); + + return $this; + } + + if (is_string($types) && is_string($key)) { + $types = [$key => $types]; + } + $this->_parts['set']->eq($key, $value, $types); + + return $this; + } + + /** + * Create a delete query. + * + * Can be combined with from(), where() and other methods to + * create delete queries with specific conditions. + * + * @param string|null $table The table to use when deleting. + * @return $this + */ + public function delete($table = null) + { + $this->_dirty(); + $this->_type = 'delete'; + if ($table !== null) { + $this->from($table); + } + + return $this; + } + + /** + * A string or expression that will be appended to the generated query + * + * ### Examples: + * ``` + * $query->select('id')->where(['author_id' => 1])->epilog('FOR UPDATE'); + * $query + * ->insert('articles', ['title']) + * ->values(['author_id' => 1]) + * ->epilog('RETURNING id'); + * ``` + * + * Epliog content is raw SQL and not suitable for use with user supplied data. + * + * @param string|\Cake\Database\Expression\QueryExpression|null $expression The expression to be appended + * @return $this + */ + public function epilog($expression = null) + { + $this->_dirty(); + $this->_parts['epilog'] = $expression; + + return $this; + } + + /** + * Returns the type of this query (select, insert, update, delete) + * + * @return string + */ + public function type() + { + return $this->_type; + } + + /** + * Returns a new QueryExpression object. This is a handy function when + * building complex queries using a fluent interface. You can also override + * this function in subclasses to use a more specialized QueryExpression class + * if required. + * + * You can optionally pass a single raw SQL string or an array or expressions in + * any format accepted by \Cake\Database\Expression\QueryExpression: + * + * ``` + * $expression = $query->newExpr(); // Returns an empty expression object + * $expression = $query->newExpr('Table.column = Table2.column'); // Return a raw SQL expression + * ``` + * + * @param mixed $rawExpression A string, array or anything you want wrapped in an expression object + * @return \Cake\Database\Expression\QueryExpression + */ + public function newExpr($rawExpression = null) + { + $expression = new QueryExpression([], $this->getTypeMap()); + + if ($rawExpression !== null) { + $expression->add($rawExpression); + } + + return $expression; + } + + /** + * Returns an instance of a functions builder object that can be used for + * generating arbitrary SQL functions. + * + * ### Example: + * + * ``` + * $query->func()->count('*'); + * $query->func()->dateDiff(['2012-01-05', '2012-01-02']) + * ``` + * + * @return \Cake\Database\FunctionsBuilder + */ + public function func() + { + if ($this->_functionsBuilder === null) { + $this->_functionsBuilder = new FunctionsBuilder(); + } + + return $this->_functionsBuilder; + } + + /** + * Executes this query and returns a results iterator. This function is required + * for implementing the IteratorAggregate interface and allows the query to be + * iterated without having to call execute() manually, thus making it look like + * a result set instead of the query itself. + * + * @return \Cake\Database\StatementInterface|null + */ + public function getIterator() + { + if ($this->_iterator === null || $this->_dirty) { + $this->_iterator = $this->execute(); + } + + return $this->_iterator; + } + + /** + * Returns any data that was stored in the specified clause. This is useful for + * modifying any internal part of the query and it is used by the SQL dialects + * to transform the query accordingly before it is executed. The valid clauses that + * can be retrieved are: delete, update, set, insert, values, select, distinct, + * from, join, set, where, group, having, order, limit, offset and union. + * + * The return value for each of those parts may vary. Some clauses use QueryExpression + * to internally store their state, some use arrays and others may use booleans or + * integers. This is summary of the return types for each clause. + * + * - update: string The name of the table to update + * - set: QueryExpression + * - insert: array, will return an array containing the table + columns. + * - values: ValuesExpression + * - select: array, will return empty array when no fields are set + * - distinct: boolean + * - from: array of tables + * - join: array + * - set: array + * - where: QueryExpression, returns null when not set + * - group: array + * - having: QueryExpression, returns null when not set + * - order: OrderByExpression, returns null when not set + * - limit: integer or QueryExpression, null when not set + * - offset: integer or QueryExpression, null when not set + * - union: array + * + * @param string $name name of the clause to be returned + * @return mixed + * @throws \InvalidArgumentException When the named clause does not exist. + */ + public function clause($name) + { + if (!array_key_exists($name, $this->_parts)) { + $clauses = implode(', ', array_keys($this->_parts)); + throw new InvalidArgumentException("The '$name' clause is not defined. Valid clauses are: $clauses"); + } + + return $this->_parts[$name]; + } + + /** + * Registers a callback to be executed for each result that is fetched from the + * result set, the callback function will receive as first parameter an array with + * the raw data from the database for every row that is fetched and must return the + * row with any possible modifications. + * + * Callbacks will be executed lazily, if only 3 rows are fetched for database it will + * called 3 times, event though there might be more rows to be fetched in the cursor. + * + * Callbacks are stacked in the order they are registered, if you wish to reset the stack + * the call this function with the second parameter set to true. + * + * If you wish to remove all decorators from the stack, set the first parameter + * to null and the second to true. + * + * ### Example + * + * ``` + * $query->decorateResults(function ($row) { + * $row['order_total'] = $row['subtotal'] + ($row['subtotal'] * $row['tax']); + * return $row; + * }); + * ``` + * + * @param callable|null $callback The callback to invoke when results are fetched. + * @param bool $overwrite Whether or not this should append or replace all existing decorators. + * @return $this + */ + public function decorateResults($callback, $overwrite = false) + { + if ($overwrite) { + $this->_resultDecorators = []; + } + + if ($callback !== null) { + $this->_resultDecorators[] = $callback; + } + + return $this; + } + + /** + * This function works similar to the traverse() function, with the difference + * that it does a full depth traversal of the entire expression tree. This will execute + * the provided callback function for each ExpressionInterface object that is + * stored inside this query at any nesting depth in any part of the query. + * + * Callback will receive as first parameter the currently visited expression. + * + * @param callable $callback the function to be executed for each ExpressionInterface + * found inside this query. + * @return $this|null + */ + public function traverseExpressions(callable $callback) + { + $visitor = function ($expression) use (&$visitor, $callback) { + if (is_array($expression)) { + foreach ($expression as $e) { + $visitor($e); + } + + return null; + } + + if ($expression instanceof ExpressionInterface) { + $expression->traverse($visitor); + + if (!($expression instanceof self)) { + $callback($expression); + } + } + }; + + return $this->traverse($visitor); + } + + /** + * Associates a query placeholder to a value and a type. + * + * If type is expressed as "atype[]" (note braces) then it will cause the + * placeholder to be re-written dynamically so if the value is an array, it + * will create as many placeholders as values are in it. For example: + * + * ``` + * $query->bind(':id', [1, 2, 3], 'int[]'); + * ``` + * + * Will create 3 int placeholders. When using named placeholders, this method + * requires that the placeholders include `:` e.g. `:value`. + * + * @param string|int $param placeholder to be replaced with quoted version + * of $value + * @param mixed $value The value to be bound + * @param string|int $type the mapped type name, used for casting when sending + * to database + * @return $this + */ + public function bind($param, $value, $type = 'string') + { + $this->getValueBinder()->bind($param, $value, $type); + + return $this; + } + + /** + * Returns the currently used ValueBinder instance. + * + * A ValueBinder is responsible for generating query placeholders and temporarily + * associate values to those placeholders so that they can be passed correctly + * to the statement object. + * + * @return \Cake\Database\ValueBinder + */ + public function getValueBinder() + { + if ($this->_valueBinder === null) { + $this->_valueBinder = new ValueBinder(); + } + + return $this->_valueBinder; + } + + /** + * Overwrite the current value binder + * + * A ValueBinder is responsible for generating query placeholders and temporarily + * associate values to those placeholders so that they can be passed correctly + * to the statement object. + * + * @param \Cake\Database\ValueBinder|bool $binder The binder or false to disable binding. + * @return $this + */ + public function setValueBinder($binder) + { + $this->_valueBinder = $binder; + + return $this; + } + + /** + * Returns the currently used ValueBinder instance. If a value is passed, + * it will be set as the new instance to be used. + * + * A ValueBinder is responsible for generating query placeholders and temporarily + * associate values to those placeholders so that they can be passed correctly + * to the statement object. + * + * @deprecated 3.5.0 Use setValueBinder()/getValueBinder() instead. + * @param \Cake\Database\ValueBinder|false|null $binder new instance to be set. If no value is passed the + * default one will be returned + * @return $this|\Cake\Database\ValueBinder + */ + public function valueBinder($binder = null) + { + deprecationWarning('Query::valueBinder() is deprecated. Use Query::getValueBinder()/setValueBinder() instead.'); + if ($binder === null) { + if ($this->_valueBinder === null) { + $this->_valueBinder = new ValueBinder(); + } + + return $this->_valueBinder; + } + $this->_valueBinder = $binder; + + return $this; + } + + /** + * Enables/Disables buffered results. + * + * When enabled the results returned by this Query will be + * buffered. This enables you to iterate a result set multiple times, or + * both cache and iterate it. + * + * When disabled it will consume less memory as fetched results are not + * remembered for future iterations. + * + * @param bool $enable Whether or not to enable buffering + * @return $this + */ + public function enableBufferedResults($enable = true) + { + $this->_dirty(); + $this->_useBufferedResults = (bool)$enable; + + return $this; + } + + /** + * Returns whether buffered results are enabled/disabled. + * + * When enabled the results returned by this Query will be + * buffered. This enables you to iterate a result set multiple times, or + * both cache and iterate it. + * + * When disabled it will consume less memory as fetched results are not + * remembered for future iterations. + * + * @return bool + */ + public function isBufferedResultsEnabled() + { + return $this->_useBufferedResults; + } + + /** + * Enable/Disable buffered results. + * + * When enabled the results returned by this Query will be + * buffered. This enables you to iterate a result set multiple times, or + * both cache and iterate it. + * + * When disabled it will consume less memory as fetched results are not + * remembered for future iterations. + * + * If called with no arguments, it will return whether or not buffering is + * enabled. + * + * @deprecated 3.4.0 Use enableBufferedResults()/isBufferedResultsEnabled() instead. + * @param bool|null $enable Whether or not to enable buffering + * @return bool|$this + */ + public function bufferResults($enable = null) + { + deprecationWarning( + 'Query::bufferResults() is deprecated. ' . + 'Use Query::enableBufferedResults()/isBufferedResultsEnabled() instead.' + ); + if ($enable !== null) { + return $this->enableBufferedResults($enable); + } + + return $this->isBufferedResultsEnabled(); + } + + /** + * Sets the TypeMap class where the types for each of the fields in the + * select clause are stored. + * + * @param \Cake\Database\TypeMap $typeMap The map object to use + * @return $this + */ + public function setSelectTypeMap(TypeMap $typeMap) + { + $this->_selectTypeMap = $typeMap; + $this->_dirty(); + + return $this; + } + + /** + * Gets the TypeMap class where the types for each of the fields in the + * select clause are stored. + * + * @return \Cake\Database\TypeMap + */ + public function getSelectTypeMap() + { + if ($this->_selectTypeMap === null) { + $this->_selectTypeMap = new TypeMap(); + } + + return $this->_selectTypeMap; + } + + /** + * Disables the automatic casting of fields to their corresponding PHP data type + * + * @return $this + */ + public function disableResultsCasting() + { + $this->typeCastEnabled = false; + + return $this; + } + + /** + * Enables the automatic casting of fields to their corresponding type + * + * @return $this + */ + public function enableResultsCasting() + { + $this->typeCastEnabled = true; + + return $this; + } + + /** + * Sets the TypeMap class where the types for each of the fields in the + * select clause are stored. + * + * When called with no arguments, the current TypeMap object is returned. + * + * @deprecated 3.4.0 Use setSelectTypeMap()/getSelectTypeMap() instead. + * @param \Cake\Database\TypeMap|null $typeMap The map object to use + * @return $this|\Cake\Database\TypeMap + */ + public function selectTypeMap(TypeMap $typeMap = null) + { + deprecationWarning( + 'Query::selectTypeMap() is deprecated. ' . + 'Use Query::setSelectTypeMap()/getSelectTypeMap() instead.' + ); + if ($typeMap !== null) { + return $this->setSelectTypeMap($typeMap); + } + + return $this->getSelectTypeMap(); + } + + /** + * Auxiliary function used to wrap the original statement from the driver with + * any registered callbacks. + * + * @param \Cake\Database\StatementInterface $statement to be decorated + * @return \Cake\Database\Statement\CallbackStatement + */ + protected function _decorateStatement($statement) + { + $typeMap = $this->getSelectTypeMap(); + $driver = $this->getConnection()->getDriver(); + + if ($this->typeCastEnabled && $typeMap->toArray()) { + $statement = new CallbackStatement($statement, $driver, new FieldTypeConverter($typeMap, $driver)); + } + + foreach ($this->_resultDecorators as $f) { + $statement = new CallbackStatement($statement, $driver, $f); + } + + return $statement; + } + + /** + * Helper function used to build conditions by composing QueryExpression objects. + * + * @param string $part Name of the query part to append the new part to + * @param string|null|array|\Cake\Database\ExpressionInterface|callable $append Expression or builder function to append. + * @param string $conjunction type of conjunction to be used to operate part + * @param array $types associative array of type names used to bind values to query + * @return void + */ + protected function _conjugate($part, $append, $conjunction, $types) + { + $expression = $this->_parts[$part] ?: $this->newExpr(); + if (empty($append)) { + $this->_parts[$part] = $expression; + + return; + } + + if ($expression->isCallable($append)) { + $append = $append($this->newExpr(), $this); + } + + if ($expression->getConjunction() === $conjunction) { + $expression->add($append, $types); + } else { + $expression = $this->newExpr() + ->setConjunction($conjunction) + ->add([$expression, $append], $types); + } + + $this->_parts[$part] = $expression; + $this->_dirty(); + } + + /** + * Marks a query as dirty, removing any preprocessed information + * from in memory caching. + * + * @return void + */ + protected function _dirty() + { + $this->_dirty = true; + + if ($this->_iterator && $this->_valueBinder) { + $this->getValueBinder()->reset(); + } + } + + /** + * Do a deep clone on this object. + * + * Will clone all of the expression objects used in + * each of the clauses, as well as the valueBinder. + * + * @return void + */ + public function __clone() + { + $this->_iterator = null; + if ($this->_valueBinder !== null) { + $this->_valueBinder = clone $this->_valueBinder; + } + if ($this->_selectTypeMap !== null) { + $this->_selectTypeMap = clone $this->_selectTypeMap; + } + foreach ($this->_parts as $name => $part) { + if (empty($part)) { + continue; + } + if (is_array($part)) { + foreach ($part as $i => $piece) { + if ($piece instanceof ExpressionInterface) { + $this->_parts[$name][$i] = clone $piece; + } + } + } + if ($part instanceof ExpressionInterface) { + $this->_parts[$name] = clone $part; + } + } + } + + /** + * Returns string representation of this query (complete SQL statement). + * + * @return string + */ + public function __toString() + { + return $this->sql(); + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + try { + set_error_handler(function ($errno, $errstr) { + throw new RuntimeException($errstr, $errno); + }, E_ALL); + $sql = $this->sql(); + $params = $this->getValueBinder()->bindings(); + } catch (RuntimeException $e) { + $sql = 'SQL could not be generated for this query as it is incomplete.'; + $params = []; + } finally { + restore_error_handler(); + } + + return [ + '(help)' => 'This is a Query object, to get the results execute or iterate it.', + 'sql' => $sql, + 'params' => $params, + 'defaultTypes' => $this->getDefaultTypes(), + 'decorators' => count($this->_resultDecorators), + 'executed' => $this->_iterator ? true : false + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/QueryCompiler.php b/app/vendor/cakephp/cakephp/src/Database/QueryCompiler.php new file mode 100644 index 000000000..79f5c6791 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/QueryCompiler.php @@ -0,0 +1,392 @@ + 'DELETE', + 'where' => ' WHERE %s', + 'group' => ' GROUP BY %s ', + 'having' => ' HAVING %s ', + 'order' => ' %s', + 'limit' => ' LIMIT %s', + 'offset' => ' OFFSET %s', + 'epilog' => ' %s' + ]; + + /** + * The list of query clauses to traverse for generating a SELECT statement + * + * @var array + */ + protected $_selectParts = [ + 'select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit', + 'offset', 'union', 'epilog' + ]; + + /** + * The list of query clauses to traverse for generating an UPDATE statement + * + * @var array + */ + protected $_updateParts = ['update', 'set', 'where', 'epilog']; + + /** + * The list of query clauses to traverse for generating a DELETE statement + * + * @var array + */ + protected $_deleteParts = ['delete', 'modifier', 'from', 'where', 'epilog']; + + /** + * The list of query clauses to traverse for generating an INSERT statement + * + * @var array + */ + protected $_insertParts = ['insert', 'values', 'epilog']; + + /** + * Indicate whether or not this query dialect supports ordered unions. + * + * Overridden in subclasses. + * + * @var bool + */ + protected $_orderedUnion = true; + + /** + * Returns the SQL representation of the provided query after generating + * the placeholders for the bound values using the provided generator + * + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @return \Closure + */ + public function compile(Query $query, ValueBinder $generator) + { + $sql = ''; + $type = $query->type(); + $query->traverse( + $this->_sqlCompiler($sql, $query, $generator), + $this->{'_' . $type . 'Parts'} + ); + + // Propagate bound parameters from sub-queries if the + // placeholders can be found in the SQL statement. + if ($query->getValueBinder() !== $generator) { + foreach ($query->getValueBinder()->bindings() as $binding) { + $placeholder = ':' . $binding['placeholder']; + if (preg_match('/' . $placeholder . '(?:\W|$)/', $sql) > 0) { + $generator->bind($placeholder, $binding['value'], $binding['type']); + } + } + } + + return $sql; + } + + /** + * Returns a callable object that can be used to compile a SQL string representation + * of this query. + * + * @param string $sql initial sql string to append to + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator The placeholder and value binder object + * @return \Closure + */ + protected function _sqlCompiler(&$sql, $query, $generator) + { + return function ($parts, $name) use (&$sql, $query, $generator) { + if (!isset($parts) || + ((is_array($parts) || $parts instanceof \Countable) && !count($parts)) + ) { + return; + } + if ($parts instanceof ExpressionInterface) { + $parts = [$parts->sql($generator)]; + } + if (isset($this->_templates[$name])) { + $parts = $this->_stringifyExpressions((array)$parts, $generator); + + return $sql .= sprintf($this->_templates[$name], implode(', ', $parts)); + } + + return $sql .= $this->{'_build' . ucfirst($name) . 'Part'}($parts, $query, $generator); + }; + } + + /** + * Helper function used to build the string representation of a SELECT clause, + * it constructs the field list taking care of aliasing and + * converting expression objects to string. This function also constructs the + * DISTINCT clause for the query. + * + * @param array $parts list of fields to be transformed to string + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @return string + */ + protected function _buildSelectPart($parts, $query, $generator) + { + $driver = $query->getConnection()->getDriver(); + $select = 'SELECT%s %s%s'; + if ($this->_orderedUnion && $query->clause('union')) { + $select = '(SELECT%s %s%s'; + } + $distinct = $query->clause('distinct'); + $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $generator); + + $normalized = []; + $parts = $this->_stringifyExpressions($parts, $generator); + foreach ($parts as $k => $p) { + if (!is_numeric($k)) { + $p = $p . ' AS ' . $driver->quoteIdentifier($k); + } + $normalized[] = $p; + } + + if ($distinct === true) { + $distinct = 'DISTINCT '; + } + + if (is_array($distinct)) { + $distinct = $this->_stringifyExpressions($distinct, $generator); + $distinct = sprintf('DISTINCT ON (%s) ', implode(', ', $distinct)); + } + + return sprintf($select, $modifiers, $distinct, implode(', ', $normalized)); + } + + /** + * Helper function used to build the string representation of a FROM clause, + * it constructs the tables list taking care of aliasing and + * converting expression objects to string. + * + * @param array $parts list of tables to be transformed to string + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @return string + */ + protected function _buildFromPart($parts, $query, $generator) + { + $select = ' FROM %s'; + $normalized = []; + $parts = $this->_stringifyExpressions($parts, $generator); + foreach ($parts as $k => $p) { + if (!is_numeric($k)) { + $p = $p . ' ' . $k; + } + $normalized[] = $p; + } + + return sprintf($select, implode(', ', $normalized)); + } + + /** + * Helper function used to build the string representation of multiple JOIN clauses, + * it constructs the joins list taking care of aliasing and converting + * expression objects to string in both the table to be joined and the conditions + * to be used. + * + * @param array $parts list of joins to be transformed to string + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @return string + */ + protected function _buildJoinPart($parts, $query, $generator) + { + $joins = ''; + foreach ($parts as $join) { + $subquery = $join['table'] instanceof Query || $join['table'] instanceof QueryExpression; + if ($join['table'] instanceof ExpressionInterface) { + $join['table'] = $join['table']->sql($generator); + } + + if ($subquery) { + $join['table'] = '(' . $join['table'] . ')'; + } + + $joins .= sprintf(' %s JOIN %s %s', $join['type'], $join['table'], $join['alias']); + + $condition = ''; + if (isset($join['conditions']) && $join['conditions'] instanceof ExpressionInterface) { + $condition = $join['conditions']->sql($generator); + } + if (strlen($condition)) { + $joins .= " ON {$condition}"; + } else { + $joins .= ' ON 1 = 1'; + } + } + + return $joins; + } + + /** + * Helper function to generate SQL for SET expressions. + * + * @param array $parts List of keys & values to set. + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @return string + */ + protected function _buildSetPart($parts, $query, $generator) + { + $set = []; + foreach ($parts as $part) { + if ($part instanceof ExpressionInterface) { + $part = $part->sql($generator); + } + if ($part[0] === '(') { + $part = substr($part, 1, -1); + } + $set[] = $part; + } + + return ' SET ' . implode('', $set); + } + + /** + * Builds the SQL string for all the UNION clauses in this query, when dealing + * with query objects it will also transform them using their configured SQL + * dialect. + * + * @param array $parts list of queries to be operated with UNION + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @return string + */ + protected function _buildUnionPart($parts, $query, $generator) + { + $parts = array_map(function ($p) use ($generator) { + $p['query'] = $p['query']->sql($generator); + $p['query'] = $p['query'][0] === '(' ? trim($p['query'], '()') : $p['query']; + $prefix = $p['all'] ? 'ALL ' : ''; + if ($this->_orderedUnion) { + return "{$prefix}({$p['query']})"; + } + + return $prefix . $p['query']; + }, $parts); + + if ($this->_orderedUnion) { + return sprintf(")\nUNION %s", implode("\nUNION ", $parts)); + } + + return sprintf("\nUNION %s", implode("\nUNION ", $parts)); + } + + /** + * Builds the SQL fragment for INSERT INTO. + * + * @param array $parts The insert parts. + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @return string SQL fragment. + */ + protected function _buildInsertPart($parts, $query, $generator) + { + $table = $parts[0]; + $columns = $this->_stringifyExpressions($parts[1], $generator); + $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $generator); + + return sprintf('INSERT%s INTO %s (%s)', $modifiers, $table, implode(', ', $columns)); + } + + /** + * Builds the SQL fragment for INSERT INTO. + * + * @param array $parts The values parts. + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @return string SQL fragment. + */ + protected function _buildValuesPart($parts, $query, $generator) + { + return implode('', $this->_stringifyExpressions($parts, $generator)); + } + + /** + * Builds the SQL fragment for UPDATE. + * + * @param array $parts The update parts. + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @return string SQL fragment. + */ + protected function _buildUpdatePart($parts, $query, $generator) + { + $table = $this->_stringifyExpressions($parts, $generator); + $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $generator); + + return sprintf('UPDATE%s %s', $modifiers, implode(',', $table)); + } + + /** + * Builds the SQL modifier fragment + * + * @param array $parts The query modifier parts + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @return string SQL fragment. + */ + protected function _buildModifierPart($parts, $query, $generator) + { + if ($parts === []) { + return ''; + } + + return ' ' . implode(' ', $this->_stringifyExpressions($parts, $generator, false)); + } + + /** + * Helper function used to covert ExpressionInterface objects inside an array + * into their string representation. + * + * @param array $expressions list of strings and ExpressionInterface objects + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @param bool $wrap Whether to wrap each expression object with parenthesis + * @return array + */ + protected function _stringifyExpressions($expressions, $generator, $wrap = true) + { + $result = []; + foreach ($expressions as $k => $expression) { + if ($expression instanceof ExpressionInterface) { + $value = $expression->sql($generator); + $expression = $wrap ? '(' . $value . ')' : $value; + } + $result[$k] = $expression; + } + + return $result; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/README.md b/app/vendor/cakephp/cakephp/src/Database/README.md new file mode 100644 index 000000000..c545e318d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/README.md @@ -0,0 +1,364 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/database.svg?style=flat-square)](https://packagist.org/packages/cakephp/database) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# A flexible and lightweight Database Library for PHP + +This library abstracts and provides help with most aspects of dealing with relational +databases such as keeping connections to the server, building queries, +preventing SQL injections, inspecting and altering schemas, and with debugging and +profiling queries sent to the database. + +It adopts the API from the native PDO extension in PHP for familiarity, but solves many of the +inconsistencies PDO has, while also providing several features that extend PDO's capabilities. + +A distinguishing factor of this library when compared to similar database connection packages, +is that it takes the concept of "data types" to its core. It lets you work with complex PHP objects +or structures that can be passed as query conditions or to be inserted in the database. + +The typing system will intelligently convert the PHP structures when passing them to the database, and +convert them back when retrieving. + + +## Connecting to the database + +This library is able to work with the following databases: + +* MySQL +* Postgres +* SQLite +* Microsoft SQL Server (2008 and above) + +The first thing you need to do when using this library is create a connection object. +Before performing any operations with the connection, you need to specify a driver +to use: + +```php +use Cake\Database\Connection; +use Cake\Database\Driver\Mysql; + +$driver = new Mysql([ + 'database' => 'test', + 'username' => 'root', + 'password' => 'secret' +]); +$connection = new Connection([ + 'driver' => $driver +]); +``` + +Drivers are classes responsible for actually executing the commands to the database and +correctly building the SQL according to the database specific dialect. Drivers can also +be specified by passing a class name. In that case, include all the connection details +directly in the options array: + +```php +use Cake\Database\Connection; + +$connection = new Connection([ + 'driver' => 'Cake\Database\Driver\Sqlite', + 'database' => '/path/to/file.db' +]); +``` + +### Connection options + +This is a list of possible options that can be passed when creating a connection: + +* `persistent`: Creates a persistent connection +* `host`: The server host +* `database`: The database name +* `username`: Login credential +* `password`: Connection secret +* `encoding`: The connection encoding (or charset) +* `timezone`: The connection timezone or time offset + +## Using connections + +After creating a connection, you can immediately interact with the database. You can choose +either to use the shorthand methods `execute()`, `insert()`, `update()`, `delete()` or use the +`newQuery()` for using a query builder. + +The easiest way of executing queries is by using the `execute()` method, it will return a +`Cake\Database\StatementInterface` that you can use to get the data back: + +```php +$statement = $connection->execute('SELECT * FROM articles'); + +while($row = $statement->fetch('assoc')) { + echo $row['title'] . PHP_EOL; +} +``` +Binding values to parametrized arguments is also possible with the execute function: + +```php +$statement = $connection->execute('SELECT * FROM articles WHERE id = :id', ['id' => 1], ['id' => 'integer']); +$results = $statement->fetch('assoc'); +``` + +The third parameter is the types the passed values should be converted to when passed to the database. If +no types are passed, all arguments will be interpreted as a string. + +Alternatively you can construct a statement manually and then fetch rows from it: + +```php +$statement = $connection->prepare('SELECT * from articles WHERE id != :id'); +$statement->bind(['id' => 1], ['id' => 'integer']); +$results = $statement->fetchAll('assoc'); +``` + +The default types that are understood by this library and can be passed to the `bind()` function or to `execute()` +are: + +* biginteger +* binary +* date +* float +* decimal +* integer +* time +* datetime +* timestamp +* uuid + +More types can be added dynamically in a bit. + +Statements can be reused by binding new values to the parameters in the query: + +```php +$statement = $connection->prepare('SELECT * from articles WHERE id = :id'); +$statement->bind(['id' => 1], ['id' => 'integer']); +$results = $statement->fetchAll('assoc'); + +$statement->bind(['id' => 1], ['id' => 'integer']); +$results = $statement->fetchAll('assoc'); +``` + +### Updating Rows + +Updating can be done using the `update()` function in the connection object. In the following +example we will update the title of the article with id = 1: + +```php +$connection->update('articles', ['title' => 'New title'], ['id' => 1]); +``` + +The concept of data types is central to this library, so you can use the last parameter of the function +to specify what types should be used: + +```php +$connection->update( + 'articles', + ['title' => 'New title'], + ['created >=' => new DateTime('-3 day'), 'created <' => new DateTime('now')], + ['created' => 'datetime'] +); +``` + +The example above will execute the following SQL: + +```sql +UPDATE articles SET title = 'New Title' WHERE created >= '2014-10-10 00:00:00' AND created < '2014-10-13 00:00:00'; +``` + +More on creating complex where conditions or more complex update queries later. + +### Deleting Rows + +Similarly, the `delete()` method is used to delete rows from the database: + +```php +$connection->delete('articles', ['created <' => DateTime('now')], ['created' => 'date']); +``` + +Will generate the following SQL + +```sql +DELETE FROM articles where created < '2014-10-10' +``` + +### Inserting Rows + +Rows can be inserted using the `insert()` method: + +```php +$connection->insert( + 'articles', + ['title' => 'My Title', 'body' => 'Some paragraph', 'created' => new DateTime()], + ['created' => 'datetime'] +); +``` + +More complex updates, deletes and insert queries can be generated using the `Query` class. + +## Query Builder + +One of the goals of this library is to allow the generation of both simple and complex queries with +ease. The query builder can be accessed by getting a new instance of a query: + +```php +$query = $connection->newQuery(); +``` + +### Selecting Fields + +Adding fields to the `SELECT` clause: + +```php +$query->select(['id', 'title', 'body']); + +// Results in SELECT id AS pk, title AS aliased_title, body ... +$query->select(['pk' => 'id', 'aliased_title' => 'title', 'body']); + +// Use a closure +$query->select(function ($query) { + return ['id', 'title', 'body']; +}); +``` + +### Where Conditions + +Generating conditions: + +```php +// WHERE id = 1 +$query->where(['id' => 1]); + +// WHERE id > 2 +$query->where(['id >' => 1]); +``` + +As you can see you can use any operator by placing it with a space after the field name. +Adding multiple conditions is easy as well: + +```php +$query->where(['id >' => 1])->andWhere(['title' => 'My Title']); + +// Equivalent to +$query->where(['id >' => 1, 'title' => 'My title']); +``` + +It is possible to generate `OR` conditions as well + +```php +$query->where(['OR' => ['id >' => 1, 'title' => 'My title']]); +``` + +For even more complex conditions you can use closures and expression objects: + +```php +$query->where(function ($exp) { + return $exp + ->eq('author_id', 2) + ->eq('published', true) + ->notEq('spam', true) + ->gt('view_count', 10); + }); +``` + +Which results in: + +```sql +SELECT * FROM articles +WHERE + author_id = 2 + AND published = 1 + AND spam != 1 + AND view_count > 10 +``` + +Combining expressions is also possible: + +```php +$query->where(function ($exp) { + $orConditions = $exp->or_(['author_id' => 2]) + ->eq('author_id', 5); + return $exp + ->not($orConditions) + ->lte('view_count', 10); + }); +``` + +That generates: + +```sql +SELECT * +FROM articles +WHERE + NOT (author_id = 2 OR author_id = 5) + AND view_count <= 10 +``` + +When using the expression objects you can use the following methods to create conditions: + +* `eq()` Creates an equality condition. +* `notEq()` Create an inequality condition +* `like()` Create a condition using the LIKE operator. +* `notLike()` Create a negated LIKE condition. +* `in()` Create a condition using IN. +* `notIn()` Create a negated condition using IN. +* `gt()` Create a > condition. +* `gte()` Create a >= condition. +* `lt()` Create a < condition. +* `lte()` Create a <= condition. +* `isNull()` Create an IS NULL condition. +* `isNotNull()` Create a negated IS NULL condition. + +### Aggregates and SQL Functions + +```php +// Results in SELECT COUNT(*) count FROM ... +$query->select(['count' => $query->func()->count('*')]); +``` + +A number of commonly used functions can be created with the func() method: + +* `sum()` Calculate a sum. The arguments will be treated as literal values. +* `avg()` Calculate an average. The arguments will be treated as literal values. +* `min()` Calculate the min of a column. The arguments will be treated as literal values. +* `max()` Calculate the max of a column. The arguments will be treated as literal values. +* `count()` Calculate the count. The arguments will be treated as literal values. +* `concat()` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal. +* `coalesce()` Coalesce values. The arguments are treated as bound parameters unless marked as literal. +* `dateDiff()` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal. +* `now()` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date. + +When providing arguments for SQL functions, there are two kinds of parameters you can use, literal arguments and bound parameters. Literal +parameters allow you to reference columns or other SQL literals. Bound parameters can be used to safely add user data to SQL functions. +For example: + +```php +$concat = $query->func()->concat([ + 'title' => 'literal', + ' NEW' +]); +$query->select(['title' => $concat]); +``` + +The above generates: + +```sql +SELECT CONCAT(title, :c0) ...; +``` + +### Other SQL Clauses + +Read of all other SQL clauses that the builder is capable of generating in the [official API docs](https://api.cakephp.org/3.x/class-Cake.Database.Query.html) + +### Getting Results out of a Query + +Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this: + +```php +// Iterate the query +foreach ($query as $row) { + // Do stuff. +} + +// Get the statement and fetch all results +$results = $query->execute()->fetchAll('assoc'); +``` + +## Official API + +You can read the official [official API docs](https://api.cakephp.org/3.x/namespace-Cake.Database.html) to learn more of what this library +has to offer. diff --git a/app/vendor/cakephp/cakephp/src/Database/Retry/ReconnectStrategy.php b/app/vendor/cakephp/cakephp/src/Database/Retry/ReconnectStrategy.php new file mode 100644 index 000000000..37eb364d5 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Retry/ReconnectStrategy.php @@ -0,0 +1,122 @@ +connection = $connection; + } + + /** + * Checks whether or not the exception was caused by a lost connection, + * and returns true if it was able to successfully reconnect. + * + * @param Exception $exception The exception to check for its message + * @param int $retryCount The number of times the action has been already called + * @return bool Whether or not it is OK to retry the action + */ + public function shouldRetry(Exception $exception, $retryCount) + { + $message = $exception->getMessage(); + + foreach (static::$causes as $cause) { + if (strstr($message, $cause) !== false) { + return $this->reconnect(); + } + } + + return false; + } + + /** + * Tries to re-establish the connection to the server, if it is safe to do so + * + * @return bool Whether or not the connection was re-established + */ + protected function reconnect() + { + if ($this->connection->inTransaction()) { + // It is not safe to blindly reconnect in the middle of a transaction + return false; + } + + try { + // Make sure we free any resources associated with the old connection + $this->connection->disconnect(); + } catch (Exception $e) { + } + + try { + $this->connection->connect(); + $this->connection->log('[RECONNECT]'); + + return true; + } catch (Exception $e) { + // If there was an error connecting again, don't report it back, + // let the retry handler do it. + return false; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/BaseSchema.php b/app/vendor/cakephp/cakephp/src/Database/Schema/BaseSchema.php new file mode 100644 index 000000000..281c2208e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Schema/BaseSchema.php @@ -0,0 +1,276 @@ +connect(); + $this->_driver = $driver; + } + + /** + * Generate an ON clause for a foreign key. + * + * @param string|null $on The on clause + * @return string + */ + protected function _foreignOnClause($on) + { + if ($on === TableSchema::ACTION_SET_NULL) { + return 'SET NULL'; + } + if ($on === TableSchema::ACTION_SET_DEFAULT) { + return 'SET DEFAULT'; + } + if ($on === TableSchema::ACTION_CASCADE) { + return 'CASCADE'; + } + if ($on === TableSchema::ACTION_RESTRICT) { + return 'RESTRICT'; + } + if ($on === TableSchema::ACTION_NO_ACTION) { + return 'NO ACTION'; + } + } + + /** + * Convert string on clauses to the abstract ones. + * + * @param string $clause The on clause to convert. + * @return string|null + */ + protected function _convertOnClause($clause) + { + if ($clause === 'CASCADE' || $clause === 'RESTRICT') { + return strtolower($clause); + } + if ($clause === 'NO ACTION') { + return TableSchema::ACTION_NO_ACTION; + } + + return TableSchema::ACTION_SET_NULL; + } + + /** + * Convert foreign key constraints references to a valid + * stringified list + * + * @param string|array $references The referenced columns of a foreign key constraint statement + * @return string + */ + protected function _convertConstraintColumns($references) + { + if (is_string($references)) { + return $this->_driver->quoteIdentifier($references); + } + + return implode(', ', array_map( + [$this->_driver, 'quoteIdentifier'], + $references + )); + } + + /** + * Generate the SQL to drop a table. + * + * @param \Cake\Database\Schema\TableSchema $schema Schema instance + * @return array SQL statements to drop a table. + */ + public function dropTableSql(TableSchema $schema) + { + $sql = sprintf( + 'DROP TABLE %s', + $this->_driver->quoteIdentifier($schema->name()) + ); + + return [$sql]; + } + + /** + * Generate the SQL to list the tables. + * + * @param array $config The connection configuration to use for + * getting tables from. + * @return array An array of (sql, params) to execute. + */ + abstract public function listTablesSql($config); + + /** + * Generate the SQL to describe a table. + * + * @param string $tableName The table name to get information on. + * @param array $config The connection configuration. + * @return array An array of (sql, params) to execute. + */ + abstract public function describeColumnSql($tableName, $config); + + /** + * Generate the SQL to describe the indexes in a table. + * + * @param string $tableName The table name to get information on. + * @param array $config The connection configuration. + * @return array An array of (sql, params) to execute. + */ + abstract public function describeIndexSql($tableName, $config); + + /** + * Generate the SQL to describe the foreign keys in a table. + * + * @param string $tableName The table name to get information on. + * @param array $config The connection configuration. + * @return array An array of (sql, params) to execute. + */ + abstract public function describeForeignKeySql($tableName, $config); + + /** + * Generate the SQL to describe table options + * + * @param string $tableName Table name. + * @param array $config The connection configuration. + * @return array SQL statements to get options for a table. + */ + public function describeOptionsSql($tableName, $config) + { + return ['', '']; + } + + /** + * Convert field description results into abstract schema fields. + * + * @param \Cake\Database\Schema\TableSchema $schema The table object to append fields to. + * @param array $row The row data from `describeColumnSql`. + * @return void + */ + abstract public function convertColumnDescription(TableSchema $schema, $row); + + /** + * Convert an index description results into abstract schema indexes or constraints. + * + * @param \Cake\Database\Schema\TableSchema $schema The table object to append + * an index or constraint to. + * @param array $row The row data from `describeIndexSql`. + * @return void + */ + abstract public function convertIndexDescription(TableSchema $schema, $row); + + /** + * Convert a foreign key description into constraints on the Table object. + * + * @param \Cake\Database\Schema\TableSchema $schema The table object to append + * a constraint to. + * @param array $row The row data from `describeForeignKeySql`. + * @return void + */ + abstract public function convertForeignKeyDescription(TableSchema $schema, $row); + + /** + * Convert options data into table options. + * + * @param \Cake\Database\Schema\TableSchema $schema Table instance. + * @param array $row The row of data. + * @return void + */ + public function convertOptionsDescription(TableSchema $schema, $row) + { + } + + /** + * Generate the SQL to create a table. + * + * @param \Cake\Database\Schema\TableSchema $schema Table instance. + * @param array $columns The columns to go inside the table. + * @param array $constraints The constraints for the table. + * @param array $indexes The indexes for the table. + * @return array SQL statements to create a table. + */ + abstract public function createTableSql(TableSchema $schema, $columns, $constraints, $indexes); + + /** + * Generate the SQL fragment for a single column in a table. + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in. + * @param string $name The name of the column. + * @return string SQL fragment. + */ + abstract public function columnSql(TableSchema $schema, $name); + + /** + * Generate the SQL queries needed to add foreign key constraints to the table + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are. + * @return array SQL fragment. + */ + abstract public function addConstraintSql(TableSchema $schema); + + /** + * Generate the SQL queries needed to drop foreign key constraints from the table + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are. + * @return array SQL fragment. + */ + abstract public function dropConstraintSql(TableSchema $schema); + + /** + * Generate the SQL fragments for defining table constraints. + * + * @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in. + * @param string $name The name of the column. + * @return string SQL fragment. + */ + abstract public function constraintSql(TableSchema $schema, $name); + + /** + * Generate the SQL fragment for a single index in a table. + * + * @param \Cake\Database\Schema\TableSchema $schema The table object the column is in. + * @param string $name The name of the column. + * @return string SQL fragment. + */ + abstract public function indexSql(TableSchema $schema, $name); + + /** + * Generate the SQL to truncate a table. + * + * @param \Cake\Database\Schema\TableSchema $schema Table instance. + * @return array SQL statements to truncate a table. + */ + abstract public function truncateTableSql(TableSchema $schema); +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/CachedCollection.php b/app/vendor/cakephp/cakephp/src/Database/Schema/CachedCollection.php new file mode 100644 index 000000000..8ec484f02 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Schema/CachedCollection.php @@ -0,0 +1,132 @@ +setCacheMetadata($cacheKey); + } + + /** + * {@inheritDoc} + * + */ + public function describe($name, array $options = []) + { + $options += ['forceRefresh' => false]; + $cacheConfig = $this->getCacheMetadata(); + $cacheKey = $this->cacheKey($name); + + if (!empty($cacheConfig) && !$options['forceRefresh']) { + $cached = Cache::read($cacheKey, $cacheConfig); + if ($cached !== false) { + return $cached; + } + } + + $table = parent::describe($name, $options); + + if (!empty($cacheConfig)) { + Cache::write($cacheKey, $table, $cacheConfig); + } + + return $table; + } + + /** + * Get the cache key for a given name. + * + * @param string $name The name to get a cache key for. + * @return string The cache key. + */ + public function cacheKey($name) + { + return $this->_connection->configName() . '_' . $name; + } + + /** + * Sets the cache config name to use for caching table metadata, or + * disables it if false is passed. + * + * @param bool $enable Whether or not to enable caching + * @return $this + */ + public function setCacheMetadata($enable) + { + if ($enable === true) { + $enable = '_cake_model_'; + } + + $this->_cache = $enable; + + return $this; + } + + /** + * Gets the cache config name to use for caching table metadata, false means disabled. + * + * @return string|bool + */ + public function getCacheMetadata() + { + return $this->_cache; + } + + /** + * Sets the cache config name to use for caching table metadata, or + * disables it if false is passed. + * If called with no arguments it returns the current configuration name. + * + * @deprecated 3.4.0 Use setCacheMetadata()/getCacheMetadata() + * @param bool|null $enable Whether or not to enable caching + * @return string|bool + */ + public function cacheMetadata($enable = null) + { + deprecationWarning( + 'CachedCollection::cacheMetadata() is deprecated. ' . + 'Use CachedCollection::setCacheMetadata()/getCacheMetadata() instead.' + ); + if ($enable !== null) { + $this->setCacheMetadata($enable); + } + + return $this->getCacheMetadata(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/Collection.php b/app/vendor/cakephp/cakephp/src/Database/Schema/Collection.php new file mode 100644 index 000000000..483bc9a3b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Schema/Collection.php @@ -0,0 +1,138 @@ +_connection = $connection; + $this->_dialect = $connection->getDriver()->schemaDialect(); + } + + /** + * Get the list of tables available in the current connection. + * + * @return array The list of tables in the connected database/schema. + */ + public function listTables() + { + list($sql, $params) = $this->_dialect->listTablesSql($this->_connection->config()); + $result = []; + $statement = $this->_connection->execute($sql, $params); + while ($row = $statement->fetch()) { + $result[] = $row[0]; + } + $statement->closeCursor(); + + return $result; + } + + /** + * Get the column metadata for a table. + * + * Caching will be applied if `cacheMetadata` key is present in the Connection + * configuration options. Defaults to _cake_model_ when true. + * + * ### Options + * + * - `forceRefresh` - Set to true to force rebuilding the cached metadata. + * Defaults to false. + * + * @param string $name The name of the table to describe. + * @param array $options The options to use, see above. + * @return \Cake\Database\Schema\TableSchema Object with column metadata. + * @throws \Cake\Database\Exception when table cannot be described. + */ + public function describe($name, array $options = []) + { + $config = $this->_connection->config(); + if (strpos($name, '.')) { + list($config['schema'], $name) = explode('.', $name); + } + $table = new TableSchema($name); + + $this->_reflect('Column', $name, $config, $table); + if (count($table->columns()) === 0) { + throw new Exception(sprintf('Cannot describe %s. It has 0 columns.', $name)); + } + + $this->_reflect('Index', $name, $config, $table); + $this->_reflect('ForeignKey', $name, $config, $table); + $this->_reflect('Options', $name, $config, $table); + + return $table; + } + + /** + * Helper method for running each step of the reflection process. + * + * @param string $stage The stage name. + * @param string $name The table name. + * @param array $config The config data. + * @param \Cake\Database\Schema\TableSchema $schema The table instance + * @return void + * @throws \Cake\Database\Exception on query failure. + */ + protected function _reflect($stage, $name, $config, $schema) + { + $describeMethod = "describe{$stage}Sql"; + $convertMethod = "convert{$stage}Description"; + + list($sql, $params) = $this->_dialect->{$describeMethod}($name, $config); + if (empty($sql)) { + return; + } + try { + $statement = $this->_connection->execute($sql, $params); + } catch (PDOException $e) { + throw new Exception($e->getMessage(), 500, $e); + } + foreach ($statement->fetchAll('assoc') as $row) { + $this->_dialect->{$convertMethod}($schema, $row); + } + $statement->closeCursor(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/MysqlSchema.php b/app/vendor/cakephp/cakephp/src/Database/Schema/MysqlSchema.php new file mode 100644 index 000000000..5ca25bf96 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Schema/MysqlSchema.php @@ -0,0 +1,564 @@ +_driver->quoteIdentifier($config['database']), []]; + } + + /** + * {@inheritDoc} + */ + public function describeColumnSql($tableName, $config) + { + return ['SHOW FULL COLUMNS FROM ' . $this->_driver->quoteIdentifier($tableName), []]; + } + + /** + * {@inheritDoc} + */ + public function describeIndexSql($tableName, $config) + { + return ['SHOW INDEXES FROM ' . $this->_driver->quoteIdentifier($tableName), []]; + } + + /** + * {@inheritDoc} + */ + public function describeOptionsSql($tableName, $config) + { + return ['SHOW TABLE STATUS WHERE Name = ?', [$tableName]]; + } + + /** + * {@inheritDoc} + */ + public function convertOptionsDescription(TableSchema $schema, $row) + { + $schema->setOptions([ + 'engine' => $row['Engine'], + 'collation' => $row['Collation'], + ]); + } + + /** + * Convert a MySQL column type into an abstract type. + * + * The returned type will be a type that Cake\Database\Type can handle. + * + * @param string $column The column type + length + * @return array Array of column information. + * @throws \Cake\Database\Exception When column type cannot be parsed. + */ + protected function _convertColumn($column) + { + preg_match('/([a-z]+)(?:\(([0-9,]+)\))?\s*([a-z]+)?/i', $column, $matches); + if (empty($matches)) { + throw new Exception(sprintf('Unable to parse column type from "%s"', $column)); + } + + $col = strtolower($matches[1]); + $length = $precision = null; + if (isset($matches[2])) { + $length = $matches[2]; + if (strpos($matches[2], ',') !== false) { + list($length, $precision) = explode(',', $length); + } + $length = (int)$length; + $precision = (int)$precision; + } + + if (in_array($col, ['date', 'time', 'datetime', 'timestamp'])) { + return ['type' => $col, 'length' => null]; + } + if (($col === 'tinyint' && $length === 1) || $col === 'boolean') { + return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null]; + } + + $unsigned = (isset($matches[3]) && strtolower($matches[3]) === 'unsigned'); + if (strpos($col, 'bigint') !== false || $col === 'bigint') { + return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if ($col === 'tinyint') { + return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if ($col === 'smallint') { + return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if (in_array($col, ['int', 'integer', 'mediumint'])) { + return ['type' => TableSchema::TYPE_INTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if ($col === 'char' && $length === 36) { + return ['type' => TableSchema::TYPE_UUID, 'length' => null]; + } + if ($col === 'char') { + return ['type' => TableSchema::TYPE_STRING, 'fixed' => true, 'length' => $length]; + } + if (strpos($col, 'char') !== false) { + return ['type' => TableSchema::TYPE_STRING, 'length' => $length]; + } + if (strpos($col, 'text') !== false) { + $lengthName = substr($col, 0, -4); + $length = isset(TableSchema::$columnLengths[$lengthName]) ? TableSchema::$columnLengths[$lengthName] : null; + + return ['type' => TableSchema::TYPE_TEXT, 'length' => $length]; + } + if ($col === 'binary' && $length === 16) { + return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null]; + } + if (strpos($col, 'blob') !== false || $col === 'binary') { + $lengthName = substr($col, 0, -4); + $length = isset(TableSchema::$columnLengths[$lengthName]) ? TableSchema::$columnLengths[$lengthName] : null; + + return ['type' => TableSchema::TYPE_BINARY, 'length' => $length]; + } + if (strpos($col, 'float') !== false || strpos($col, 'double') !== false) { + return [ + 'type' => TableSchema::TYPE_FLOAT, + 'length' => $length, + 'precision' => $precision, + 'unsigned' => $unsigned + ]; + } + if (strpos($col, 'decimal') !== false) { + return [ + 'type' => TableSchema::TYPE_DECIMAL, + 'length' => $length, + 'precision' => $precision, + 'unsigned' => $unsigned + ]; + } + + if (strpos($col, 'json') !== false) { + return ['type' => TableSchema::TYPE_JSON, 'length' => null]; + } + + return ['type' => TableSchema::TYPE_STRING, 'length' => null]; + } + + /** + * {@inheritDoc} + */ + public function convertColumnDescription(TableSchema $schema, $row) + { + $field = $this->_convertColumn($row['Type']); + $field += [ + 'null' => $row['Null'] === 'YES', + 'default' => $row['Default'], + 'collate' => $row['Collation'], + 'comment' => $row['Comment'], + ]; + if (isset($row['Extra']) && $row['Extra'] === 'auto_increment') { + $field['autoIncrement'] = true; + } + $schema->addColumn($row['Field'], $field); + } + + /** + * {@inheritDoc} + */ + public function convertIndexDescription(TableSchema $schema, $row) + { + $type = null; + $columns = $length = []; + + $name = $row['Key_name']; + if ($name === 'PRIMARY') { + $name = $type = TableSchema::CONSTRAINT_PRIMARY; + } + + $columns[] = $row['Column_name']; + + if ($row['Index_type'] === 'FULLTEXT') { + $type = TableSchema::INDEX_FULLTEXT; + } elseif ($row['Non_unique'] == 0 && $type !== 'primary') { + $type = TableSchema::CONSTRAINT_UNIQUE; + } elseif ($type !== 'primary') { + $type = TableSchema::INDEX_INDEX; + } + + if (!empty($row['Sub_part'])) { + $length[$row['Column_name']] = $row['Sub_part']; + } + $isIndex = ( + $type === TableSchema::INDEX_INDEX || + $type === TableSchema::INDEX_FULLTEXT + ); + if ($isIndex) { + $existing = $schema->getIndex($name); + } else { + $existing = $schema->getConstraint($name); + } + + // MySQL multi column indexes come back as multiple rows. + if (!empty($existing)) { + $columns = array_merge($existing['columns'], $columns); + $length = array_merge($existing['length'], $length); + } + if ($isIndex) { + $schema->addIndex($name, [ + 'type' => $type, + 'columns' => $columns, + 'length' => $length + ]); + } else { + $schema->addConstraint($name, [ + 'type' => $type, + 'columns' => $columns, + 'length' => $length + ]); + } + } + + /** + * {@inheritDoc} + */ + public function describeForeignKeySql($tableName, $config) + { + $sql = 'SELECT * FROM information_schema.key_column_usage AS kcu + INNER JOIN information_schema.referential_constraints AS rc + ON ( + kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA + ) + WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ? AND rc.TABLE_NAME = ?'; + + return [$sql, [$config['database'], $tableName, $tableName]]; + } + + /** + * {@inheritDoc} + */ + public function convertForeignKeyDescription(TableSchema $schema, $row) + { + $data = [ + 'type' => TableSchema::CONSTRAINT_FOREIGN, + 'columns' => [$row['COLUMN_NAME']], + 'references' => [$row['REFERENCED_TABLE_NAME'], $row['REFERENCED_COLUMN_NAME']], + 'update' => $this->_convertOnClause($row['UPDATE_RULE']), + 'delete' => $this->_convertOnClause($row['DELETE_RULE']), + ]; + $name = $row['CONSTRAINT_NAME']; + $schema->addConstraint($name, $data); + } + + /** + * {@inheritDoc} + */ + public function truncateTableSql(TableSchema $schema) + { + return [sprintf('TRUNCATE TABLE `%s`', $schema->name())]; + } + + /** + * {@inheritDoc} + */ + public function createTableSql(TableSchema $schema, $columns, $constraints, $indexes) + { + $content = implode(",\n", array_merge($columns, $constraints, $indexes)); + $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' '; + $content = sprintf("CREATE%sTABLE `%s` (\n%s\n)", $temporary, $schema->name(), $content); + $options = $schema->getOptions(); + if (isset($options['engine'])) { + $content .= sprintf(' ENGINE=%s', $options['engine']); + } + if (isset($options['charset'])) { + $content .= sprintf(' DEFAULT CHARSET=%s', $options['charset']); + } + if (isset($options['collate'])) { + $content .= sprintf(' COLLATE=%s', $options['collate']); + } + + return [$content]; + } + + /** + * {@inheritDoc} + */ + public function columnSql(TableSchema $schema, $name) + { + $data = $schema->getColumn($name); + $out = $this->_driver->quoteIdentifier($name); + $nativeJson = $this->_driver->supportsNativeJson(); + + $typeMap = [ + TableSchema::TYPE_TINYINTEGER => ' TINYINT', + TableSchema::TYPE_SMALLINTEGER => ' SMALLINT', + TableSchema::TYPE_INTEGER => ' INTEGER', + TableSchema::TYPE_BIGINTEGER => ' BIGINT', + TableSchema::TYPE_BINARY_UUID => ' BINARY(16)', + TableSchema::TYPE_BOOLEAN => ' BOOLEAN', + TableSchema::TYPE_FLOAT => ' FLOAT', + TableSchema::TYPE_DECIMAL => ' DECIMAL', + TableSchema::TYPE_DATE => ' DATE', + TableSchema::TYPE_TIME => ' TIME', + TableSchema::TYPE_DATETIME => ' DATETIME', + TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP', + TableSchema::TYPE_UUID => ' CHAR(36)', + TableSchema::TYPE_JSON => $nativeJson ? ' JSON' : ' LONGTEXT' + ]; + $specialMap = [ + 'string' => true, + 'text' => true, + 'binary' => true, + ]; + if (isset($typeMap[$data['type']])) { + $out .= $typeMap[$data['type']]; + } + if (isset($specialMap[$data['type']])) { + switch ($data['type']) { + case TableSchema::TYPE_STRING: + $out .= !empty($data['fixed']) ? ' CHAR' : ' VARCHAR'; + if (!isset($data['length'])) { + $data['length'] = 255; + } + break; + case TableSchema::TYPE_TEXT: + $isKnownLength = in_array($data['length'], TableSchema::$columnLengths); + if (empty($data['length']) || !$isKnownLength) { + $out .= ' TEXT'; + break; + } + + if ($isKnownLength) { + $length = array_search($data['length'], TableSchema::$columnLengths); + $out .= ' ' . strtoupper($length) . 'TEXT'; + } + + break; + case TableSchema::TYPE_BINARY: + $isKnownLength = in_array($data['length'], TableSchema::$columnLengths); + if (empty($data['length']) || !$isKnownLength) { + $out .= ' BLOB'; + break; + } + + if ($isKnownLength) { + $length = array_search($data['length'], TableSchema::$columnLengths); + $out .= ' ' . strtoupper($length) . 'BLOB'; + } + + break; + } + } + $hasLength = [ + TableSchema::TYPE_INTEGER, + TableSchema::TYPE_SMALLINTEGER, + TableSchema::TYPE_TINYINTEGER, + TableSchema::TYPE_STRING + ]; + if (in_array($data['type'], $hasLength, true) && isset($data['length'])) { + $out .= '(' . (int)$data['length'] . ')'; + } + + $hasPrecision = [TableSchema::TYPE_FLOAT, TableSchema::TYPE_DECIMAL]; + if (in_array($data['type'], $hasPrecision, true) && + (isset($data['length']) || isset($data['precision'])) + ) { + $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')'; + } + + $hasUnsigned = [ + TableSchema::TYPE_TINYINTEGER, + TableSchema::TYPE_SMALLINTEGER, + TableSchema::TYPE_INTEGER, + TableSchema::TYPE_BIGINTEGER, + TableSchema::TYPE_FLOAT, + TableSchema::TYPE_DECIMAL + ]; + if (in_array($data['type'], $hasUnsigned, true) && + isset($data['unsigned']) && $data['unsigned'] === true + ) { + $out .= ' UNSIGNED'; + } + + $hasCollate = [ + TableSchema::TYPE_TEXT, + TableSchema::TYPE_STRING, + ]; + if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') { + $out .= ' COLLATE ' . $data['collate']; + } + + if (isset($data['null']) && $data['null'] === false) { + $out .= ' NOT NULL'; + } + $addAutoIncrement = ( + [$name] == (array)$schema->primaryKey() && + !$schema->hasAutoincrement() && + !isset($data['autoIncrement']) + ); + if (in_array($data['type'], [TableSchema::TYPE_INTEGER, TableSchema::TYPE_BIGINTEGER]) && + ($data['autoIncrement'] === true || $addAutoIncrement) + ) { + $out .= ' AUTO_INCREMENT'; + } + if (isset($data['null']) && $data['null'] === true && $data['type'] === TableSchema::TYPE_TIMESTAMP) { + $out .= ' NULL'; + unset($data['default']); + } + if (isset($data['default']) && + in_array($data['type'], [TableSchema::TYPE_TIMESTAMP, TableSchema::TYPE_DATETIME]) && + in_array(strtolower($data['default']), ['current_timestamp', 'current_timestamp()']) + ) { + $out .= ' DEFAULT CURRENT_TIMESTAMP'; + unset($data['default']); + } + if (isset($data['default'])) { + $out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']); + unset($data['default']); + } + if (isset($data['comment']) && $data['comment'] !== '') { + $out .= ' COMMENT ' . $this->_driver->schemaValue($data['comment']); + } + + return $out; + } + + /** + * {@inheritDoc} + */ + public function constraintSql(TableSchema $schema, $name) + { + $data = $schema->getConstraint($name); + if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) { + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + + return sprintf('PRIMARY KEY (%s)', implode(', ', $columns)); + } + + $out = ''; + if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) { + $out = 'UNIQUE KEY '; + } + if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $out = 'CONSTRAINT '; + } + $out .= $this->_driver->quoteIdentifier($name); + + return $this->_keySql($out, $data); + } + + /** + * {@inheritDoc} + */ + public function addConstraintSql(TableSchema $schema) + { + $sqlPattern = 'ALTER TABLE %s ADD %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name)); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function dropConstraintSql(TableSchema $schema) + { + $sqlPattern = 'ALTER TABLE %s DROP FOREIGN KEY %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $constraintName = $this->_driver->quoteIdentifier($name); + $sql[] = sprintf($sqlPattern, $tableName, $constraintName); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function indexSql(TableSchema $schema, $name) + { + $data = $schema->getIndex($name); + $out = ''; + if ($data['type'] === TableSchema::INDEX_INDEX) { + $out = 'KEY '; + } + if ($data['type'] === TableSchema::INDEX_FULLTEXT) { + $out = 'FULLTEXT KEY '; + } + $out .= $this->_driver->quoteIdentifier($name); + + return $this->_keySql($out, $data); + } + + /** + * Helper method for generating key SQL snippets. + * + * @param string $prefix The key prefix + * @param array $data Key data. + * @return string + */ + protected function _keySql($prefix, $data) + { + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + foreach ($data['columns'] as $i => $column) { + if (isset($data['length'][$column])) { + $columns[$i] .= sprintf('(%d)', $data['length'][$column]); + } + } + if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) { + return $prefix . sprintf( + ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s', + implode(', ', $columns), + $this->_driver->quoteIdentifier($data['references'][0]), + $this->_convertConstraintColumns($data['references'][1]), + $this->_foreignOnClause($data['update']), + $this->_foreignOnClause($data['delete']) + ); + } + + return $prefix . ' (' . implode(', ', $columns) . ')'; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/PostgresSchema.php b/app/vendor/cakephp/cakephp/src/Database/Schema/PostgresSchema.php new file mode 100644 index 000000000..0a5a2742a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Schema/PostgresSchema.php @@ -0,0 +1,600 @@ + $col, 'length' => null]; + } + if (strpos($col, 'timestamp') !== false) { + return ['type' => TableSchema::TYPE_TIMESTAMP, 'length' => null]; + } + if (strpos($col, 'time') !== false) { + return ['type' => TableSchema::TYPE_TIME, 'length' => null]; + } + if ($col === 'serial' || $col === 'integer') { + return ['type' => TableSchema::TYPE_INTEGER, 'length' => 10]; + } + if ($col === 'bigserial' || $col === 'bigint') { + return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => 20]; + } + if ($col === 'smallint') { + return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => 5]; + } + if ($col === 'inet') { + return ['type' => TableSchema::TYPE_STRING, 'length' => 39]; + } + if ($col === 'uuid') { + return ['type' => TableSchema::TYPE_UUID, 'length' => null]; + } + if ($col === 'char' || $col === 'character') { + return ['type' => TableSchema::TYPE_STRING, 'fixed' => true, 'length' => $length]; + } + // money is 'string' as it includes arbitrary text content + // before the number value. + if (strpos($col, 'char') !== false || + strpos($col, 'money') !== false + ) { + return ['type' => TableSchema::TYPE_STRING, 'length' => $length]; + } + if (strpos($col, 'text') !== false) { + return ['type' => TableSchema::TYPE_TEXT, 'length' => null]; + } + if ($col === 'bytea') { + return ['type' => TableSchema::TYPE_BINARY, 'length' => null]; + } + if ($col === 'real' || strpos($col, 'double') !== false) { + return ['type' => TableSchema::TYPE_FLOAT, 'length' => null]; + } + if (strpos($col, 'numeric') !== false || + strpos($col, 'decimal') !== false + ) { + return ['type' => TableSchema::TYPE_DECIMAL, 'length' => null]; + } + + if (strpos($col, 'json') !== false) { + return ['type' => TableSchema::TYPE_JSON, 'length' => null]; + } + + return ['type' => TableSchema::TYPE_STRING, 'length' => null]; + } + + /** + * {@inheritDoc} + */ + public function convertColumnDescription(TableSchema $schema, $row) + { + $field = $this->_convertColumn($row['type']); + + if ($field['type'] === TableSchema::TYPE_BOOLEAN) { + if ($row['default'] === 'true') { + $row['default'] = 1; + } + if ($row['default'] === 'false') { + $row['default'] = 0; + } + } + if (!empty($row['has_serial'])) { + $field['autoIncrement'] = true; + } + + $field += [ + 'default' => $this->_defaultValue($row['default']), + 'null' => $row['null'] === 'YES', + 'collate' => $row['collation_name'], + 'comment' => $row['comment'] + ]; + $field['length'] = $row['char_length'] ?: $field['length']; + + if ($field['type'] === 'numeric' || $field['type'] === 'decimal') { + $field['length'] = $row['column_precision']; + $field['precision'] = $row['column_scale'] ?: null; + } + $schema->addColumn($row['name'], $field); + } + + /** + * Manipulate the default value. + * + * Postgres includes sequence data and casting information in default values. + * We need to remove those. + * + * @param string|null $default The default value. + * @return string|null + */ + protected function _defaultValue($default) + { + if (is_numeric($default) || $default === null) { + return $default; + } + // Sequences + if (strpos($default, 'nextval') === 0) { + return null; + } + + if (strpos($default, 'NULL::') === 0) { + return null; + } + + // Remove quotes and postgres casts + return preg_replace( + "/^'(.*)'(?:::.*)$/", + '$1', + $default + ); + } + + /** + * {@inheritDoc} + */ + public function describeIndexSql($tableName, $config) + { + $sql = 'SELECT + c2.relname, + a.attname, + i.indisprimary, + i.indisunique + FROM pg_catalog.pg_namespace n + INNER JOIN pg_catalog.pg_class c ON (n.oid = c.relnamespace) + INNER JOIN pg_catalog.pg_index i ON (c.oid = i.indrelid) + INNER JOIN pg_catalog.pg_class c2 ON (c2.oid = i.indexrelid) + INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = c.oid AND i.indrelid::regclass = a.attrelid::regclass) + WHERE n.nspname = ? + AND a.attnum = ANY(i.indkey) + AND c.relname = ? + ORDER BY i.indisprimary DESC, i.indisunique DESC, c.relname, a.attnum'; + + $schema = 'public'; + if (!empty($config['schema'])) { + $schema = $config['schema']; + } + + return [$sql, [$schema, $tableName]]; + } + + /** + * {@inheritDoc} + */ + public function convertIndexDescription(TableSchema $schema, $row) + { + $type = TableSchema::INDEX_INDEX; + $name = $row['relname']; + if ($row['indisprimary']) { + $name = $type = TableSchema::CONSTRAINT_PRIMARY; + } + if ($row['indisunique'] && $type === TableSchema::INDEX_INDEX) { + $type = TableSchema::CONSTRAINT_UNIQUE; + } + if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) { + $this->_convertConstraint($schema, $name, $type, $row); + + return; + } + $index = $schema->getIndex($name); + if (!$index) { + $index = [ + 'type' => $type, + 'columns' => [] + ]; + } + $index['columns'][] = $row['attname']; + $schema->addIndex($name, $index); + } + + /** + * Add/update a constraint into the schema object. + * + * @param \Cake\Database\Schema\TableSchema $schema The table to update. + * @param string $name The index name. + * @param string $type The index type. + * @param array $row The metadata record to update with. + * @return void + */ + protected function _convertConstraint($schema, $name, $type, $row) + { + $constraint = $schema->getConstraint($name); + if (!$constraint) { + $constraint = [ + 'type' => $type, + 'columns' => [] + ]; + } + $constraint['columns'][] = $row['attname']; + $schema->addConstraint($name, $constraint); + } + + /** + * {@inheritDoc} + */ + public function describeForeignKeySql($tableName, $config) + { + $sql = 'SELECT + c.conname AS name, + c.contype AS type, + a.attname AS column_name, + c.confmatchtype AS match_type, + c.confupdtype AS on_update, + c.confdeltype AS on_delete, + c.confrelid::regclass AS references_table, + ab.attname AS references_field + FROM pg_catalog.pg_namespace n + INNER JOIN pg_catalog.pg_class cl ON (n.oid = cl.relnamespace) + INNER JOIN pg_catalog.pg_constraint c ON (n.oid = c.connamespace) + INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = cl.oid AND c.conrelid = a.attrelid AND a.attnum = ANY(c.conkey)) + INNER JOIN pg_catalog.pg_attribute ab ON (a.attrelid = cl.oid AND c.confrelid = ab.attrelid AND ab.attnum = ANY(c.confkey)) + WHERE n.nspname = ? + AND cl.relname = ? + ORDER BY name, a.attnum, ab.attnum DESC'; + + $schema = empty($config['schema']) ? 'public' : $config['schema']; + + return [$sql, [$schema, $tableName]]; + } + + /** + * {@inheritDoc} + */ + public function convertForeignKeyDescription(TableSchema $schema, $row) + { + $data = [ + 'type' => TableSchema::CONSTRAINT_FOREIGN, + 'columns' => $row['column_name'], + 'references' => [$row['references_table'], $row['references_field']], + 'update' => $this->_convertOnClause($row['on_update']), + 'delete' => $this->_convertOnClause($row['on_delete']), + ]; + $schema->addConstraint($row['name'], $data); + } + + /** + * {@inheritDoc} + */ + protected function _convertOnClause($clause) + { + if ($clause === 'r') { + return TableSchema::ACTION_RESTRICT; + } + if ($clause === 'a') { + return TableSchema::ACTION_NO_ACTION; + } + if ($clause === 'c') { + return TableSchema::ACTION_CASCADE; + } + + return TableSchema::ACTION_SET_NULL; + } + + /** + * {@inheritDoc} + */ + public function columnSql(TableSchema $schema, $name) + { + $data = $schema->getColumn($name); + $out = $this->_driver->quoteIdentifier($name); + $typeMap = [ + TableSchema::TYPE_TINYINTEGER => ' SMALLINT', + TableSchema::TYPE_SMALLINTEGER => ' SMALLINT', + TableSchema::TYPE_BINARY_UUID => ' UUID', + TableSchema::TYPE_BINARY => ' BYTEA', + TableSchema::TYPE_BOOLEAN => ' BOOLEAN', + TableSchema::TYPE_FLOAT => ' FLOAT', + TableSchema::TYPE_DECIMAL => ' DECIMAL', + TableSchema::TYPE_DATE => ' DATE', + TableSchema::TYPE_TIME => ' TIME', + TableSchema::TYPE_DATETIME => ' TIMESTAMP', + TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP', + TableSchema::TYPE_UUID => ' UUID', + TableSchema::TYPE_JSON => ' JSONB' + ]; + + if (isset($typeMap[$data['type']])) { + $out .= $typeMap[$data['type']]; + } + + if ($data['type'] === TableSchema::TYPE_INTEGER || $data['type'] === TableSchema::TYPE_BIGINTEGER) { + $type = $data['type'] === TableSchema::TYPE_INTEGER ? ' INTEGER' : ' BIGINT'; + if ([$name] === $schema->primaryKey() || $data['autoIncrement'] === true) { + $type = $data['type'] === TableSchema::TYPE_INTEGER ? ' SERIAL' : ' BIGSERIAL'; + unset($data['null'], $data['default']); + } + $out .= $type; + } + + if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) { + $out .= ' TEXT'; + } + + if ($data['type'] === TableSchema::TYPE_STRING || + ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] === TableSchema::LENGTH_TINY) + ) { + $isFixed = !empty($data['fixed']); + $type = ' VARCHAR'; + if ($isFixed) { + $type = ' CHAR'; + } + $out .= $type; + if (isset($data['length']) && $data['length'] != 36) { + $out .= '(' . (int)$data['length'] . ')'; + } + } + + $hasCollate = [TableSchema::TYPE_TEXT, TableSchema::TYPE_STRING]; + if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') { + $out .= ' COLLATE "' . $data['collate'] . '"'; + } + + if ($data['type'] === TableSchema::TYPE_FLOAT && isset($data['precision'])) { + $out .= '(' . (int)$data['precision'] . ')'; + } + + if ($data['type'] === TableSchema::TYPE_DECIMAL && + (isset($data['length']) || isset($data['precision'])) + ) { + $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')'; + } + + if (isset($data['null']) && $data['null'] === false) { + $out .= ' NOT NULL'; + } + + if (isset($data['default']) && + in_array($data['type'], [TableSchema::TYPE_TIMESTAMP, TableSchema::TYPE_DATETIME]) && + strtolower($data['default']) === 'current_timestamp' + ) { + $out .= ' DEFAULT CURRENT_TIMESTAMP'; + } elseif (isset($data['default'])) { + $defaultValue = $data['default']; + if ($data['type'] === 'boolean') { + $defaultValue = (bool)$defaultValue; + } + $out .= ' DEFAULT ' . $this->_driver->schemaValue($defaultValue); + } elseif (isset($data['null']) && $data['null'] !== false) { + $out .= ' DEFAULT NULL'; + } + + return $out; + } + + /** + * {@inheritDoc} + */ + public function addConstraintSql(TableSchema $schema) + { + $sqlPattern = 'ALTER TABLE %s ADD %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name)); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function dropConstraintSql(TableSchema $schema) + { + $sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $constraintName = $this->_driver->quoteIdentifier($name); + $sql[] = sprintf($sqlPattern, $tableName, $constraintName); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function indexSql(TableSchema $schema, $name) + { + $data = $schema->getIndex($name); + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + + return sprintf( + 'CREATE INDEX %s ON %s (%s)', + $this->_driver->quoteIdentifier($name), + $this->_driver->quoteIdentifier($schema->name()), + implode(', ', $columns) + ); + } + + /** + * {@inheritDoc} + */ + public function constraintSql(TableSchema $schema, $name) + { + $data = $schema->getConstraint($name); + $out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name); + if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) { + $out = 'PRIMARY KEY'; + } + if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) { + $out .= ' UNIQUE'; + } + + return $this->_keySql($out, $data); + } + + /** + * Helper method for generating key SQL snippets. + * + * @param string $prefix The key prefix + * @param array $data Key data. + * @return string + */ + protected function _keySql($prefix, $data) + { + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) { + return $prefix . sprintf( + ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s DEFERRABLE INITIALLY IMMEDIATE', + implode(', ', $columns), + $this->_driver->quoteIdentifier($data['references'][0]), + $this->_convertConstraintColumns($data['references'][1]), + $this->_foreignOnClause($data['update']), + $this->_foreignOnClause($data['delete']) + ); + } + + return $prefix . ' (' . implode(', ', $columns) . ')'; + } + + /** + * {@inheritDoc} + */ + public function createTableSql(TableSchema $schema, $columns, $constraints, $indexes) + { + $content = array_merge($columns, $constraints); + $content = implode(",\n", array_filter($content)); + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' '; + $out = []; + $out[] = sprintf("CREATE%sTABLE %s (\n%s\n)", $temporary, $tableName, $content); + foreach ($indexes as $index) { + $out[] = $index; + } + foreach ($schema->columns() as $column) { + $columnData = $schema->getColumn($column); + if (isset($columnData['comment'])) { + $out[] = sprintf( + 'COMMENT ON COLUMN %s.%s IS %s', + $tableName, + $this->_driver->quoteIdentifier($column), + $this->_driver->schemaValue($columnData['comment']) + ); + } + } + + return $out; + } + + /** + * {@inheritDoc} + */ + public function truncateTableSql(TableSchema $schema) + { + $name = $this->_driver->quoteIdentifier($schema->name()); + + return [ + sprintf('TRUNCATE %s RESTART IDENTITY CASCADE', $name) + ]; + } + + /** + * Generate the SQL to drop a table. + * + * @param \Cake\Database\Schema\TableSchema $schema Table instance + * @return array SQL statements to drop a table. + */ + public function dropTableSql(TableSchema $schema) + { + $sql = sprintf( + 'DROP TABLE %s CASCADE', + $this->_driver->quoteIdentifier($schema->name()) + ); + + return [$sql]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/SqlGeneratorInterface.php b/app/vendor/cakephp/cakephp/src/Database/Schema/SqlGeneratorInterface.php new file mode 100644 index 000000000..f71111dfa --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Schema/SqlGeneratorInterface.php @@ -0,0 +1,71 @@ + TableSchema::TYPE_BIGINTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if ($col == 'smallint') { + return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if ($col == 'tinyint') { + return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if (strpos($col, 'int') !== false) { + return ['type' => TableSchema::TYPE_INTEGER, 'length' => $length, 'unsigned' => $unsigned]; + } + if (strpos($col, 'decimal') !== false) { + return ['type' => TableSchema::TYPE_DECIMAL, 'length' => null, 'unsigned' => $unsigned]; + } + if (in_array($col, ['float', 'real', 'double'])) { + return ['type' => TableSchema::TYPE_FLOAT, 'length' => null, 'unsigned' => $unsigned]; + } + + if (strpos($col, 'boolean') !== false) { + return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null]; + } + + if ($col === 'char' && $length === 36) { + return ['type' => TableSchema::TYPE_UUID, 'length' => null]; + } + if ($col === 'char') { + return ['type' => TableSchema::TYPE_STRING, 'fixed' => true, 'length' => $length]; + } + if (strpos($col, 'char') !== false) { + return ['type' => TableSchema::TYPE_STRING, 'length' => $length]; + } + + if ($col === 'binary' && $length === 16) { + return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null]; + } + if (in_array($col, ['blob', 'clob'])) { + return ['type' => TableSchema::TYPE_BINARY, 'length' => null]; + } + if (in_array($col, ['date', 'time', 'timestamp', 'datetime'])) { + return ['type' => $col, 'length' => null]; + } + + return ['type' => TableSchema::TYPE_TEXT, 'length' => null]; + } + + /** + * {@inheritDoc} + */ + public function listTablesSql($config) + { + return [ + 'SELECT name FROM sqlite_master WHERE type="table" ' . + 'AND name != "sqlite_sequence" ORDER BY name', + [] + ]; + } + + /** + * {@inheritDoc} + */ + public function describeColumnSql($tableName, $config) + { + $sql = sprintf( + 'PRAGMA table_info(%s)', + $this->_driver->quoteIdentifier($tableName) + ); + + return [$sql, []]; + } + + /** + * {@inheritDoc} + */ + public function convertColumnDescription(TableSchema $schema, $row) + { + $field = $this->_convertColumn($row['type']); + $field += [ + 'null' => !$row['notnull'], + 'default' => $this->_defaultValue($row['dflt_value']), + ]; + $primary = $schema->getConstraint('primary'); + + if ($row['pk'] && empty($primary)) { + $field['null'] = false; + $field['autoIncrement'] = true; + } + + // SQLite does not support autoincrement on composite keys. + if ($row['pk'] && !empty($primary)) { + $existingColumn = $primary['columns'][0]; + $schema->addColumn($existingColumn, ['autoIncrement' => null] + $schema->getColumn($existingColumn)); + } + + $schema->addColumn($row['name'], $field); + if ($row['pk']) { + $constraint = (array)$schema->getConstraint('primary') + [ + 'type' => TableSchema::CONSTRAINT_PRIMARY, + 'columns' => [] + ]; + $constraint['columns'] = array_merge($constraint['columns'], [$row['name']]); + $schema->addConstraint('primary', $constraint); + } + } + + /** + * Manipulate the default value. + * + * Sqlite includes quotes and bared NULLs in default values. + * We need to remove those. + * + * @param string|null $default The default value. + * @return string|null + */ + protected function _defaultValue($default) + { + if ($default === 'NULL') { + return null; + } + + // Remove quotes + if (preg_match("/^'(.*)'$/", $default, $matches)) { + return str_replace("''", "'", $matches[1]); + } + + return $default; + } + + /** + * {@inheritDoc} + */ + public function describeIndexSql($tableName, $config) + { + $sql = sprintf( + 'PRAGMA index_list(%s)', + $this->_driver->quoteIdentifier($tableName) + ); + + return [$sql, []]; + } + + /** + * {@inheritDoc} + * + * Since SQLite does not have a way to get metadata about all indexes at once, + * additional queries are done here. Sqlite constraint names are not + * stable, and the names for constraints will not match those used to create + * the table. This is a limitation in Sqlite's metadata features. + * + */ + public function convertIndexDescription(TableSchema $schema, $row) + { + $sql = sprintf( + 'PRAGMA index_info(%s)', + $this->_driver->quoteIdentifier($row['name']) + ); + $statement = $this->_driver->prepare($sql); + $statement->execute(); + $columns = []; + foreach ($statement->fetchAll('assoc') as $column) { + $columns[] = $column['name']; + } + $statement->closeCursor(); + if ($row['unique']) { + $schema->addConstraint($row['name'], [ + 'type' => TableSchema::CONSTRAINT_UNIQUE, + 'columns' => $columns + ]); + } else { + $schema->addIndex($row['name'], [ + 'type' => TableSchema::INDEX_INDEX, + 'columns' => $columns + ]); + } + } + + /** + * {@inheritDoc} + */ + public function describeForeignKeySql($tableName, $config) + { + $sql = sprintf('PRAGMA foreign_key_list(%s)', $this->_driver->quoteIdentifier($tableName)); + + return [$sql, []]; + } + + /** + * {@inheritDoc} + */ + public function convertForeignKeyDescription(TableSchema $schema, $row) + { + $name = $row['from'] . '_fk'; + + $update = isset($row['on_update']) ? $row['on_update'] : ''; + $delete = isset($row['on_delete']) ? $row['on_delete'] : ''; + $data = [ + 'type' => TableSchema::CONSTRAINT_FOREIGN, + 'columns' => [$row['from']], + 'references' => [$row['table'], $row['to']], + 'update' => $this->_convertOnClause($update), + 'delete' => $this->_convertOnClause($delete), + ]; + + if (isset($this->_constraintsIdMap[$schema->name()][$row['id']])) { + $name = $this->_constraintsIdMap[$schema->name()][$row['id']]; + } else { + $this->_constraintsIdMap[$schema->name()][$row['id']] = $name; + } + + $schema->addConstraint($name, $data); + } + + /** + * {@inheritDoc} + * + * @throws \Cake\Database\Exception when the column type is unknown + */ + public function columnSql(TableSchema $schema, $name) + { + $data = $schema->getColumn($name); + $typeMap = [ + TableSchema::TYPE_BINARY_UUID => ' BINARY(16)', + TableSchema::TYPE_UUID => ' CHAR(36)', + TableSchema::TYPE_TINYINTEGER => ' TINYINT', + TableSchema::TYPE_SMALLINTEGER => ' SMALLINT', + TableSchema::TYPE_INTEGER => ' INTEGER', + TableSchema::TYPE_BIGINTEGER => ' BIGINT', + TableSchema::TYPE_BOOLEAN => ' BOOLEAN', + TableSchema::TYPE_BINARY => ' BLOB', + TableSchema::TYPE_FLOAT => ' FLOAT', + TableSchema::TYPE_DECIMAL => ' DECIMAL', + TableSchema::TYPE_DATE => ' DATE', + TableSchema::TYPE_TIME => ' TIME', + TableSchema::TYPE_DATETIME => ' DATETIME', + TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP', + TableSchema::TYPE_JSON => ' TEXT' + ]; + + $out = $this->_driver->quoteIdentifier($name); + $hasUnsigned = [ + TableSchema::TYPE_TINYINTEGER, + TableSchema::TYPE_SMALLINTEGER, + TableSchema::TYPE_INTEGER, + TableSchema::TYPE_BIGINTEGER, + TableSchema::TYPE_FLOAT, + TableSchema::TYPE_DECIMAL + ]; + + if (in_array($data['type'], $hasUnsigned, true) && + isset($data['unsigned']) && $data['unsigned'] === true + ) { + if ($data['type'] !== TableSchema::TYPE_INTEGER || [$name] !== (array)$schema->primaryKey()) { + $out .= ' UNSIGNED'; + } + } + + if (isset($typeMap[$data['type']])) { + $out .= $typeMap[$data['type']]; + } + + if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) { + $out .= ' TEXT'; + } + + if ($data['type'] === TableSchema::TYPE_STRING || + ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] === TableSchema::LENGTH_TINY) + ) { + $out .= ' VARCHAR'; + + if (isset($data['length'])) { + $out .= '(' . (int)$data['length'] . ')'; + } + } + + $integerTypes = [ + TableSchema::TYPE_TINYINTEGER, + TableSchema::TYPE_SMALLINTEGER, + TableSchema::TYPE_INTEGER, + ]; + if (in_array($data['type'], $integerTypes, true) && + isset($data['length']) && [$name] !== (array)$schema->primaryKey() + ) { + $out .= '(' . (int)$data['length'] . ')'; + } + + $hasPrecision = [TableSchema::TYPE_FLOAT, TableSchema::TYPE_DECIMAL]; + if (in_array($data['type'], $hasPrecision, true) && + (isset($data['length']) || isset($data['precision'])) + ) { + $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')'; + } + + if (isset($data['null']) && $data['null'] === false) { + $out .= ' NOT NULL'; + } + + if ($data['type'] === TableSchema::TYPE_INTEGER && [$name] === (array)$schema->primaryKey()) { + $out .= ' PRIMARY KEY AUTOINCREMENT'; + } + + if (isset($data['null']) && $data['null'] === true && $data['type'] === TableSchema::TYPE_TIMESTAMP) { + $out .= ' DEFAULT NULL'; + } + if (isset($data['default'])) { + $out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']); + } + + return $out; + } + + /** + * {@inheritDoc} + * + * Note integer primary keys will return ''. This is intentional as Sqlite requires + * that integer primary keys be defined in the column definition. + * + */ + public function constraintSql(TableSchema $schema, $name) + { + $data = $schema->getConstraint($name); + if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY && + count($data['columns']) === 1 && + $schema->getColumn($data['columns'][0])['type'] === TableSchema::TYPE_INTEGER + ) { + return ''; + } + $clause = ''; + $type = ''; + if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) { + $type = 'PRIMARY KEY'; + } + if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) { + $type = 'UNIQUE'; + } + if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $type = 'FOREIGN KEY'; + + $clause = sprintf( + ' REFERENCES %s (%s) ON UPDATE %s ON DELETE %s', + $this->_driver->quoteIdentifier($data['references'][0]), + $this->_convertConstraintColumns($data['references'][1]), + $this->_foreignOnClause($data['update']), + $this->_foreignOnClause($data['delete']) + ); + } + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + + return sprintf( + 'CONSTRAINT %s %s (%s)%s', + $this->_driver->quoteIdentifier($name), + $type, + implode(', ', $columns), + $clause + ); + } + + /** + * {@inheritDoc} + * + * SQLite can not properly handle adding a constraint to an existing table. + * This method is no-op + */ + public function addConstraintSql(TableSchema $schema) + { + return []; + } + + /** + * {@inheritDoc} + * + * SQLite can not properly handle dropping a constraint to an existing table. + * This method is no-op + */ + public function dropConstraintSql(TableSchema $schema) + { + return []; + } + + /** + * {@inheritDoc} + */ + public function indexSql(TableSchema $schema, $name) + { + $data = $schema->getIndex($name); + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + + return sprintf( + 'CREATE INDEX %s ON %s (%s)', + $this->_driver->quoteIdentifier($name), + $this->_driver->quoteIdentifier($schema->name()), + implode(', ', $columns) + ); + } + + /** + * {@inheritDoc} + */ + public function createTableSql(TableSchema $schema, $columns, $constraints, $indexes) + { + $lines = array_merge($columns, $constraints); + $content = implode(",\n", array_filter($lines)); + $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' '; + $table = sprintf("CREATE%sTABLE \"%s\" (\n%s\n)", $temporary, $schema->name(), $content); + $out = [$table]; + foreach ($indexes as $index) { + $out[] = $index; + } + + return $out; + } + + /** + * {@inheritDoc} + */ + public function truncateTableSql(TableSchema $schema) + { + $name = $schema->name(); + $sql = []; + if ($this->hasSequences()) { + $sql[] = sprintf('DELETE FROM sqlite_sequence WHERE name="%s"', $name); + } + + $sql[] = sprintf('DELETE FROM "%s"', $name); + + return $sql; + } + + /** + * Returns whether there is any table in this connection to SQLite containing + * sequences + * + * @return bool + */ + public function hasSequences() + { + $result = $this->_driver + ->prepare('SELECT 1 FROM sqlite_master WHERE name = "sqlite_sequence"'); + $result->execute(); + $this->_hasSequences = (bool)$result->rowCount(); + $result->closeCursor(); + + return $this->_hasSequences; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/SqlserverSchema.php b/app/vendor/cakephp/cakephp/src/Database/Schema/SqlserverSchema.php new file mode 100644 index 000000000..e44e574b0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Schema/SqlserverSchema.php @@ -0,0 +1,576 @@ + $col, 'length' => null]; + } + if (strpos($col, 'datetime') !== false) { + return ['type' => TableSchema::TYPE_TIMESTAMP, 'length' => null]; + } + + if ($col === 'tinyint') { + return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => $precision ?: 3]; + } + if ($col === 'smallint') { + return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => $precision ?: 5]; + } + if ($col === 'int' || $col === 'integer') { + return ['type' => TableSchema::TYPE_INTEGER, 'length' => $precision ?: 10]; + } + if ($col === 'bigint') { + return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => $precision ?: 20]; + } + if ($col === 'bit') { + return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null]; + } + if (strpos($col, 'numeric') !== false || + strpos($col, 'money') !== false || + strpos($col, 'decimal') !== false + ) { + return ['type' => TableSchema::TYPE_DECIMAL, 'length' => $precision, 'precision' => $scale]; + } + + if ($col === 'real' || $col === 'float') { + return ['type' => TableSchema::TYPE_FLOAT, 'length' => null]; + } + // SqlServer schema reflection returns double length for unicode + // columns because internally it uses UTF16/UCS2 + if ($col === 'nvarchar' || $col === 'nchar' || $col === 'ntext') { + $length /= 2; + } + if (strpos($col, 'varchar') !== false && $length < 0) { + return ['type' => TableSchema::TYPE_TEXT, 'length' => null]; + } + + if (strpos($col, 'varchar') !== false) { + return ['type' => TableSchema::TYPE_STRING, 'length' => $length ?: 255]; + } + + if (strpos($col, 'char') !== false) { + return ['type' => TableSchema::TYPE_STRING, 'fixed' => true, 'length' => $length]; + } + + if (strpos($col, 'text') !== false) { + return ['type' => TableSchema::TYPE_TEXT, 'length' => null]; + } + + if ($col === 'image' || strpos($col, 'binary')) { + return ['type' => TableSchema::TYPE_BINARY, 'length' => null]; + } + + if ($col === 'uniqueidentifier') { + return ['type' => TableSchema::TYPE_UUID]; + } + + return ['type' => TableSchema::TYPE_STRING, 'length' => null]; + } + + /** + * {@inheritDoc} + */ + public function convertColumnDescription(TableSchema $schema, $row) + { + $field = $this->_convertColumn( + $row['type'], + $row['char_length'], + $row['precision'], + $row['scale'] + ); + if (!empty($row['default'])) { + $row['default'] = trim($row['default'], '()'); + } + if (!empty($row['autoincrement'])) { + $field['autoIncrement'] = true; + } + if ($field['type'] === TableSchema::TYPE_BOOLEAN) { + $row['default'] = (int)$row['default']; + } + + $field += [ + 'null' => $row['null'] === '1', + 'default' => $this->_defaultValue($row['default']), + 'collate' => $row['collation_name'], + ]; + $schema->addColumn($row['name'], $field); + } + + /** + * Manipulate the default value. + * + * Sqlite includes quotes and bared NULLs in default values. + * We need to remove those. + * + * @param string|null $default The default value. + * @return string|null + */ + protected function _defaultValue($default) + { + if ($default === 'NULL') { + return null; + } + + // Remove quotes + if (preg_match("/^N?'(.*)'/", $default, $matches)) { + return str_replace("''", "'", $matches[1]); + } + + return $default; + } + + /** + * {@inheritDoc} + */ + public function describeIndexSql($tableName, $config) + { + $sql = "SELECT + I.[name] AS [index_name], + IC.[index_column_id] AS [index_order], + AC.[name] AS [column_name], + I.[is_unique], I.[is_primary_key], + I.[is_unique_constraint] + FROM sys.[tables] AS T + INNER JOIN sys.[schemas] S ON S.[schema_id] = T.[schema_id] + INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id] + INNER JOIN sys.[index_columns] IC ON I.[object_id] = IC.[object_id] AND I.[index_id] = IC.[index_id] + INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id] AND IC.[column_id] = AC.[column_id] + WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP' AND T.[name] = ? AND S.[name] = ? + ORDER BY I.[index_id], IC.[index_column_id]"; + + $schema = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema']; + + return [$sql, [$tableName, $schema]]; + } + + /** + * {@inheritDoc} + */ + public function convertIndexDescription(TableSchema $schema, $row) + { + $type = TableSchema::INDEX_INDEX; + $name = $row['index_name']; + if ($row['is_primary_key']) { + $name = $type = TableSchema::CONSTRAINT_PRIMARY; + } + if ($row['is_unique_constraint'] && $type === TableSchema::INDEX_INDEX) { + $type = TableSchema::CONSTRAINT_UNIQUE; + } + + if ($type === TableSchema::INDEX_INDEX) { + $existing = $schema->getIndex($name); + } else { + $existing = $schema->getConstraint($name); + } + + $columns = [$row['column_name']]; + if (!empty($existing)) { + $columns = array_merge($existing['columns'], $columns); + } + + if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) { + $schema->addConstraint($name, [ + 'type' => $type, + 'columns' => $columns + ]); + + return; + } + $schema->addIndex($name, [ + 'type' => $type, + 'columns' => $columns + ]); + } + + /** + * {@inheritDoc} + */ + public function describeForeignKeySql($tableName, $config) + { + $sql = 'SELECT FK.[name] AS [foreign_key_name], FK.[delete_referential_action_desc] AS [delete_type], + FK.[update_referential_action_desc] AS [update_type], C.name AS [column], RT.name AS [reference_table], + RC.name AS [reference_column] + FROM sys.foreign_keys FK + INNER JOIN sys.foreign_key_columns FKC ON FKC.constraint_object_id = FK.object_id + INNER JOIN sys.tables T ON T.object_id = FKC.parent_object_id + INNER JOIN sys.tables RT ON RT.object_id = FKC.referenced_object_id + INNER JOIN sys.schemas S ON S.schema_id = T.schema_id AND S.schema_id = RT.schema_id + INNER JOIN sys.columns C ON C.column_id = FKC.parent_column_id AND C.object_id = FKC.parent_object_id + INNER JOIN sys.columns RC ON RC.column_id = FKC.referenced_column_id AND RC.object_id = FKC.referenced_object_id + WHERE FK.is_ms_shipped = 0 AND T.name = ? AND S.name = ?'; + + $schema = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema']; + + return [$sql, [$tableName, $schema]]; + } + + /** + * {@inheritDoc} + */ + public function convertForeignKeyDescription(TableSchema $schema, $row) + { + $data = [ + 'type' => TableSchema::CONSTRAINT_FOREIGN, + 'columns' => [$row['column']], + 'references' => [$row['reference_table'], $row['reference_column']], + 'update' => $this->_convertOnClause($row['update_type']), + 'delete' => $this->_convertOnClause($row['delete_type']), + ]; + $name = $row['foreign_key_name']; + $schema->addConstraint($name, $data); + } + + /** + * {@inheritDoc} + */ + protected function _foreignOnClause($on) + { + $parent = parent::_foreignOnClause($on); + + return $parent === 'RESTRICT' ? parent::_foreignOnClause(TableSchema::ACTION_SET_NULL) : $parent; + } + + /** + * {@inheritDoc} + */ + protected function _convertOnClause($clause) + { + switch ($clause) { + case 'NO_ACTION': + return TableSchema::ACTION_NO_ACTION; + case 'CASCADE': + return TableSchema::ACTION_CASCADE; + case 'SET_NULL': + return TableSchema::ACTION_SET_NULL; + case 'SET_DEFAULT': + return TableSchema::ACTION_SET_DEFAULT; + } + + return TableSchema::ACTION_SET_NULL; + } + + /** + * {@inheritDoc} + */ + public function columnSql(TableSchema $schema, $name) + { + $data = $schema->getColumn($name); + $out = $this->_driver->quoteIdentifier($name); + $typeMap = [ + TableSchema::TYPE_TINYINTEGER => ' TINYINT', + TableSchema::TYPE_SMALLINTEGER => ' SMALLINT', + TableSchema::TYPE_INTEGER => ' INTEGER', + TableSchema::TYPE_BIGINTEGER => ' BIGINT', + TableSchema::TYPE_BINARY_UUID => ' UNIQUEIDENTIFIER', + TableSchema::TYPE_BOOLEAN => ' BIT', + TableSchema::TYPE_FLOAT => ' FLOAT', + TableSchema::TYPE_DECIMAL => ' DECIMAL', + TableSchema::TYPE_DATE => ' DATE', + TableSchema::TYPE_TIME => ' TIME', + TableSchema::TYPE_DATETIME => ' DATETIME', + TableSchema::TYPE_TIMESTAMP => ' DATETIME', + TableSchema::TYPE_UUID => ' UNIQUEIDENTIFIER', + TableSchema::TYPE_JSON => ' NVARCHAR(MAX)', + ]; + + if (isset($typeMap[$data['type']])) { + $out .= $typeMap[$data['type']]; + } + + if ($data['type'] === TableSchema::TYPE_INTEGER || $data['type'] === TableSchema::TYPE_BIGINTEGER) { + if ([$name] === $schema->primaryKey() || $data['autoIncrement'] === true) { + unset($data['null'], $data['default']); + $out .= ' IDENTITY(1, 1)'; + } + } + + if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) { + $out .= ' NVARCHAR(MAX)'; + } + + if ($data['type'] === TableSchema::TYPE_BINARY) { + $out .= ' VARBINARY'; + + if ($data['length'] !== TableSchema::LENGTH_TINY) { + $out .= '(MAX)'; + } else { + $out .= sprintf('(%s)', TableSchema::LENGTH_TINY); + } + } + + if ($data['type'] === TableSchema::TYPE_STRING || + ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] === TableSchema::LENGTH_TINY) + ) { + $type = ' NVARCHAR'; + + if (!empty($data['fixed'])) { + $type = ' NCHAR'; + } + + if (!isset($data['length'])) { + $data['length'] = 255; + } + + $out .= sprintf('%s(%d)', $type, $data['length']); + } + + $hasCollate = [TableSchema::TYPE_TEXT, TableSchema::TYPE_STRING]; + if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') { + $out .= ' COLLATE ' . $data['collate']; + } + + if ($data['type'] === TableSchema::TYPE_FLOAT && isset($data['precision'])) { + $out .= '(' . (int)$data['precision'] . ')'; + } + + if ($data['type'] === TableSchema::TYPE_DECIMAL && + (isset($data['length']) || isset($data['precision'])) + ) { + $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')'; + } + + if (isset($data['null']) && $data['null'] === false) { + $out .= ' NOT NULL'; + } + + if (isset($data['default']) && + in_array($data['type'], [TableSchema::TYPE_TIMESTAMP, TableSchema::TYPE_DATETIME]) && + strtolower($data['default']) === 'current_timestamp' + ) { + $out .= ' DEFAULT CURRENT_TIMESTAMP'; + } elseif (isset($data['default'])) { + $default = is_bool($data['default']) ? (int)$data['default'] : $this->_driver->schemaValue($data['default']); + $out .= ' DEFAULT ' . $default; + } elseif (isset($data['null']) && $data['null'] !== false) { + $out .= ' DEFAULT NULL'; + } + + return $out; + } + + /** + * {@inheritDoc} + */ + public function addConstraintSql(TableSchema $schema) + { + $sqlPattern = 'ALTER TABLE %s ADD %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name)); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function dropConstraintSql(TableSchema $schema) + { + $sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;'; + $sql = []; + + foreach ($schema->constraints() as $name) { + $constraint = $schema->getConstraint($name); + if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $constraintName = $this->_driver->quoteIdentifier($name); + $sql[] = sprintf($sqlPattern, $tableName, $constraintName); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function indexSql(TableSchema $schema, $name) + { + $data = $schema->getIndex($name); + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + + return sprintf( + 'CREATE INDEX %s ON %s (%s)', + $this->_driver->quoteIdentifier($name), + $this->_driver->quoteIdentifier($schema->name()), + implode(', ', $columns) + ); + } + + /** + * {@inheritDoc} + */ + public function constraintSql(TableSchema $schema, $name) + { + $data = $schema->getConstraint($name); + $out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name); + if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) { + $out = 'PRIMARY KEY'; + } + if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) { + $out .= ' UNIQUE'; + } + + return $this->_keySql($out, $data); + } + + /** + * Helper method for generating key SQL snippets. + * + * @param string $prefix The key prefix + * @param array $data Key data. + * @return string + */ + protected function _keySql($prefix, $data) + { + $columns = array_map( + [$this->_driver, 'quoteIdentifier'], + $data['columns'] + ); + if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) { + return $prefix . sprintf( + ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s', + implode(', ', $columns), + $this->_driver->quoteIdentifier($data['references'][0]), + $this->_convertConstraintColumns($data['references'][1]), + $this->_foreignOnClause($data['update']), + $this->_foreignOnClause($data['delete']) + ); + } + + return $prefix . ' (' . implode(', ', $columns) . ')'; + } + + /** + * {@inheritDoc} + */ + public function createTableSql(TableSchema $schema, $columns, $constraints, $indexes) + { + $content = array_merge($columns, $constraints); + $content = implode(",\n", array_filter($content)); + $tableName = $this->_driver->quoteIdentifier($schema->name()); + $out = []; + $out[] = sprintf("CREATE TABLE %s (\n%s\n)", $tableName, $content); + foreach ($indexes as $index) { + $out[] = $index; + } + + return $out; + } + + /** + * {@inheritDoc} + */ + public function truncateTableSql(TableSchema $schema) + { + $name = $this->_driver->quoteIdentifier($schema->name()); + $queries = [ + sprintf('DELETE FROM %s', $name) + ]; + + // Restart identity sequences + $pk = $schema->primaryKey(); + if (count($pk) === 1) { + $column = $schema->getColumn($pk[0]); + if (in_array($column['type'], ['integer', 'biginteger'])) { + $queries[] = sprintf( + "DBCC CHECKIDENT('%s', RESEED, 0)", + $schema->name() + ); + } + } + + return $queries; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/Table.php b/app/vendor/cakephp/cakephp/src/Database/Schema/Table.php new file mode 100644 index 000000000..86d3f0fde --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Schema/Table.php @@ -0,0 +1,4 @@ + self::LENGTH_TINY, + 'medium' => self::LENGTH_MEDIUM, + 'long' => self::LENGTH_LONG + ]; + + /** + * The valid keys that can be used in a column + * definition. + * + * @var array + */ + protected static $_columnKeys = [ + 'type' => null, + 'baseType' => null, + 'length' => null, + 'precision' => null, + 'null' => null, + 'default' => null, + 'comment' => null, + ]; + + /** + * Additional type specific properties. + * + * @var array + */ + protected static $_columnExtras = [ + 'string' => [ + 'fixed' => null, + 'collate' => null, + ], + 'text' => [ + 'collate' => null, + ], + 'tinyinteger' => [ + 'unsigned' => null, + ], + 'smallinteger' => [ + 'unsigned' => null, + ], + 'integer' => [ + 'unsigned' => null, + 'autoIncrement' => null, + ], + 'biginteger' => [ + 'unsigned' => null, + 'autoIncrement' => null, + ], + 'decimal' => [ + 'unsigned' => null, + ], + 'float' => [ + 'unsigned' => null, + ], + ]; + + /** + * The valid keys that can be used in an index + * definition. + * + * @var array + */ + protected static $_indexKeys = [ + 'type' => null, + 'columns' => [], + 'length' => [], + 'references' => [], + 'update' => 'restrict', + 'delete' => 'restrict', + ]; + + /** + * Names of the valid index types. + * + * @var array + */ + protected static $_validIndexTypes = [ + self::INDEX_INDEX, + self::INDEX_FULLTEXT, + ]; + + /** + * Names of the valid constraint types. + * + * @var array + */ + protected static $_validConstraintTypes = [ + self::CONSTRAINT_PRIMARY, + self::CONSTRAINT_UNIQUE, + self::CONSTRAINT_FOREIGN, + ]; + + /** + * Names of the valid foreign key actions. + * + * @var array + */ + protected static $_validForeignKeyActions = [ + self::ACTION_CASCADE, + self::ACTION_SET_NULL, + self::ACTION_SET_DEFAULT, + self::ACTION_NO_ACTION, + self::ACTION_RESTRICT, + ]; + + /** + * Primary constraint type + * + * @var string + */ + const CONSTRAINT_PRIMARY = 'primary'; + + /** + * Unique constraint type + * + * @var string + */ + const CONSTRAINT_UNIQUE = 'unique'; + + /** + * Foreign constraint type + * + * @var string + */ + const CONSTRAINT_FOREIGN = 'foreign'; + + /** + * Index - index type + * + * @var string + */ + const INDEX_INDEX = 'index'; + + /** + * Fulltext index type + * + * @var string + */ + const INDEX_FULLTEXT = 'fulltext'; + + /** + * Foreign key cascade action + * + * @var string + */ + const ACTION_CASCADE = 'cascade'; + + /** + * Foreign key set null action + * + * @var string + */ + const ACTION_SET_NULL = 'setNull'; + + /** + * Foreign key no action + * + * @var string + */ + const ACTION_NO_ACTION = 'noAction'; + + /** + * Foreign key restrict action + * + * @var string + */ + const ACTION_RESTRICT = 'restrict'; + + /** + * Foreign key restrict default + * + * @var string + */ + const ACTION_SET_DEFAULT = 'setDefault'; + + /** + * Constructor. + * + * @param string $table The table name. + * @param array $columns The list of columns for the schema. + */ + public function __construct($table, array $columns = []) + { + $this->_table = $table; + foreach ($columns as $field => $definition) { + $this->addColumn($field, $definition); + } + } + + /** + * {@inheritDoc} + */ + public function name() + { + return $this->_table; + } + + /** + * {@inheritDoc} + */ + public function addColumn($name, $attrs) + { + if (is_string($attrs)) { + $attrs = ['type' => $attrs]; + } + $valid = static::$_columnKeys; + if (isset(static::$_columnExtras[$attrs['type']])) { + $valid += static::$_columnExtras[$attrs['type']]; + } + $attrs = array_intersect_key($attrs, $valid); + $this->_columns[$name] = $attrs + $valid; + $this->_typeMap[$name] = $this->_columns[$name]['type']; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function removeColumn($name) + { + unset($this->_columns[$name], $this->_typeMap[$name]); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function columns() + { + return array_keys($this->_columns); + } + + /** + * Get column data in the table. + * + * @param string $name The column name. + * @return array|null Column data or null. + * @deprecated 3.5.0 Use getColumn() instead. + */ + public function column($name) + { + deprecationWarning('TableSchema::column() is deprecated. Use TableSchema::getColumn() instead.'); + + return $this->getColumn($name); + } + + /** + * {@inheritDoc} + */ + public function getColumn($name) + { + if (!isset($this->_columns[$name])) { + return null; + } + $column = $this->_columns[$name]; + unset($column['baseType']); + + return $column; + } + + /** + * Sets the type of a column, or returns its current type + * if none is passed. + * + * @param string $name The column to get the type of. + * @param string|null $type The type to set the column to. + * @return string|null Either the column type or null. + * @deprecated 3.5.0 Use setColumnType()/getColumnType() instead. + */ + public function columnType($name, $type = null) + { + deprecationWarning('TableSchema::columnType() is deprecated. Use TableSchema::setColumnType() or TableSchema::getColumnType() instead.'); + + if ($type !== null) { + $this->setColumnType($name, $type); + } + + return $this->getColumnType($name); + } + + /** + * {@inheritDoc} + */ + public function getColumnType($name) + { + if (!isset($this->_columns[$name])) { + return null; + } + + return $this->_columns[$name]['type']; + } + + /** + * {@inheritDoc} + */ + public function setColumnType($name, $type) + { + if (!isset($this->_columns[$name])) { + return $this; + } + + $this->_columns[$name]['type'] = $type; + $this->_typeMap[$name] = $type; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function hasColumn($name) + { + return isset($this->_columns[$name]); + } + + /** + * {@inheritDoc} + */ + public function baseColumnType($column) + { + if (isset($this->_columns[$column]['baseType'])) { + return $this->_columns[$column]['baseType']; + } + + $type = $this->getColumnType($column); + + if ($type === null) { + return null; + } + + if (Type::getMap($type)) { + $type = Type::build($type)->getBaseType(); + } + + return $this->_columns[$column]['baseType'] = $type; + } + + /** + * {@inheritDoc} + */ + public function typeMap() + { + return $this->_typeMap; + } + + /** + * {@inheritDoc} + */ + public function isNullable($name) + { + if (!isset($this->_columns[$name])) { + return true; + } + + return ($this->_columns[$name]['null'] === true); + } + + /** + * {@inheritDoc} + */ + public function defaultValues() + { + $defaults = []; + foreach ($this->_columns as $name => $data) { + if (!array_key_exists('default', $data)) { + continue; + } + if ($data['default'] === null && $data['null'] !== true) { + continue; + } + $defaults[$name] = $data['default']; + } + + return $defaults; + } + + /** + * {@inheritDoc} + * @throws \Cake\Database\Exception + */ + public function addIndex($name, $attrs) + { + if (is_string($attrs)) { + $attrs = ['type' => $attrs]; + } + $attrs = array_intersect_key($attrs, static::$_indexKeys); + $attrs += static::$_indexKeys; + unset($attrs['references'], $attrs['update'], $attrs['delete']); + + if (!in_array($attrs['type'], static::$_validIndexTypes, true)) { + throw new Exception(sprintf('Invalid index type "%s" in index "%s" in table "%s".', $attrs['type'], $name, $this->_table)); + } + if (empty($attrs['columns'])) { + throw new Exception(sprintf('Index "%s" in table "%s" must have at least one column.', $name, $this->_table)); + } + $attrs['columns'] = (array)$attrs['columns']; + foreach ($attrs['columns'] as $field) { + if (empty($this->_columns[$field])) { + $msg = sprintf( + 'Columns used in index "%s" in table "%s" must be added to the Table schema first. ' . + 'The column "%s" was not found.', + $name, + $this->_table, + $field + ); + throw new Exception($msg); + } + } + $this->_indexes[$name] = $attrs; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function indexes() + { + return array_keys($this->_indexes); + } + + /** + * Read information about an index based on name. + * + * @param string $name The name of the index. + * @return array|null Array of index data, or null + * @deprecated 3.5.0 Use getIndex() instead. + */ + public function index($name) + { + deprecationWarning('TableSchema::index() is deprecated. Use TableSchema::getIndex() instead.'); + + return $this->getIndex($name); + } + + /** + * {@inheritDoc} + */ + public function getIndex($name) + { + if (!isset($this->_indexes[$name])) { + return null; + } + + return $this->_indexes[$name]; + } + + /** + * {@inheritDoc} + */ + public function primaryKey() + { + foreach ($this->_constraints as $name => $data) { + if ($data['type'] === static::CONSTRAINT_PRIMARY) { + return $data['columns']; + } + } + + return []; + } + + /** + * {@inheritDoc} + * @throws \Cake\Database\Exception + */ + public function addConstraint($name, $attrs) + { + if (is_string($attrs)) { + $attrs = ['type' => $attrs]; + } + $attrs = array_intersect_key($attrs, static::$_indexKeys); + $attrs += static::$_indexKeys; + if (!in_array($attrs['type'], static::$_validConstraintTypes, true)) { + throw new Exception(sprintf('Invalid constraint type "%s" in table "%s".', $attrs['type'], $this->_table)); + } + if (empty($attrs['columns'])) { + throw new Exception(sprintf('Constraints in table "%s" must have at least one column.', $this->_table)); + } + $attrs['columns'] = (array)$attrs['columns']; + foreach ($attrs['columns'] as $field) { + if (empty($this->_columns[$field])) { + $msg = sprintf( + 'Columns used in constraints must be added to the Table schema first. ' . + 'The column "%s" was not found in table "%s".', + $field, + $this->_table + ); + throw new Exception($msg); + } + } + + if ($attrs['type'] === static::CONSTRAINT_FOREIGN) { + $attrs = $this->_checkForeignKey($attrs); + + if (isset($this->_constraints[$name])) { + $this->_constraints[$name]['columns'] = array_unique(array_merge( + $this->_constraints[$name]['columns'], + $attrs['columns'] + )); + + if (isset($this->_constraints[$name]['references'])) { + $this->_constraints[$name]['references'][1] = array_unique(array_merge( + (array)$this->_constraints[$name]['references'][1], + [$attrs['references'][1]] + )); + } + + return $this; + } + } else { + unset($attrs['references'], $attrs['update'], $attrs['delete']); + } + + $this->_constraints[$name] = $attrs; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function dropConstraint($name) + { + if (isset($this->_constraints[$name])) { + unset($this->_constraints[$name]); + } + + return $this; + } + + /** + * Check whether or not a table has an autoIncrement column defined. + * + * @return bool + */ + public function hasAutoincrement() + { + foreach ($this->_columns as $column) { + if (isset($column['autoIncrement']) && $column['autoIncrement']) { + return true; + } + } + + return false; + } + + /** + * Helper method to check/validate foreign keys. + * + * @param array $attrs Attributes to set. + * @return array + * @throws \Cake\Database\Exception When foreign key definition is not valid. + */ + protected function _checkForeignKey($attrs) + { + if (count($attrs['references']) < 2) { + throw new Exception('References must contain a table and column.'); + } + if (!in_array($attrs['update'], static::$_validForeignKeyActions)) { + throw new Exception(sprintf('Update action is invalid. Must be one of %s', implode(',', static::$_validForeignKeyActions))); + } + if (!in_array($attrs['delete'], static::$_validForeignKeyActions)) { + throw new Exception(sprintf('Delete action is invalid. Must be one of %s', implode(',', static::$_validForeignKeyActions))); + } + + return $attrs; + } + + /** + * {@inheritDoc} + */ + public function constraints() + { + return array_keys($this->_constraints); + } + + /** + * Read information about a constraint based on name. + * + * @param string $name The name of the constraint. + * @return array|null Array of constraint data, or null + * @deprecated 3.5.0 Use getConstraint() instead. + */ + public function constraint($name) + { + deprecationWarning('TableSchema::constraint() is deprecated. Use TableSchema::getConstraint() instead.'); + + return $this->getConstraint($name); + } + + /** + * {@inheritDoc} + */ + public function getConstraint($name) + { + if (!isset($this->_constraints[$name])) { + return null; + } + + return $this->_constraints[$name]; + } + + /** + * {@inheritDoc} + */ + public function setOptions($options) + { + $this->_options = array_merge($this->_options, $options); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function getOptions() + { + return $this->_options; + } + + /** + * Get/set the options for a table. + * + * Table options allow you to set platform specific table level options. + * For example the engine type in MySQL. + * + * @deprecated 3.4.0 Use setOptions()/getOptions() instead. + * @param array|null $options The options to set, or null to read options. + * @return $this|array Either the TableSchema instance, or an array of options when reading. + */ + public function options($options = null) + { + deprecationWarning('TableSchema::options() is deprecated. Use TableSchema::setOptions() or TableSchema::getOptions() instead.'); + + if ($options !== null) { + return $this->setOptions($options); + } + + return $this->getOptions(); + } + + /** + * {@inheritDoc} + */ + public function setTemporary($temporary) + { + $this->_temporary = (bool)$temporary; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function isTemporary() + { + return $this->_temporary; + } + + /** + * Get/Set whether the table is temporary in the database + * + * @deprecated 3.4.0 Use setTemporary()/isTemporary() instead. + * @param bool|null $temporary whether or not the table is to be temporary + * @return $this|bool Either the TableSchema instance, the current temporary setting + */ + public function temporary($temporary = null) + { + deprecationWarning( + 'TableSchema::temporary() is deprecated. ' . + 'Use TableSchema::setTemporary()/getTemporary() instead.' + ); + if ($temporary !== null) { + return $this->setTemporary($temporary); + } + + return $this->isTemporary(); + } + + /** + * {@inheritDoc} + */ + public function createSql(Connection $connection) + { + $dialect = $connection->getDriver()->schemaDialect(); + $columns = $constraints = $indexes = []; + foreach (array_keys($this->_columns) as $name) { + $columns[] = $dialect->columnSql($this, $name); + } + foreach (array_keys($this->_constraints) as $name) { + $constraints[] = $dialect->constraintSql($this, $name); + } + foreach (array_keys($this->_indexes) as $name) { + $indexes[] = $dialect->indexSql($this, $name); + } + + return $dialect->createTableSql($this, $columns, $constraints, $indexes); + } + + /** + * {@inheritDoc} + */ + public function dropSql(Connection $connection) + { + $dialect = $connection->getDriver()->schemaDialect(); + + return $dialect->dropTableSql($this); + } + + /** + * {@inheritDoc} + */ + public function truncateSql(Connection $connection) + { + $dialect = $connection->getDriver()->schemaDialect(); + + return $dialect->truncateTableSql($this); + } + + /** + * {@inheritDoc} + */ + public function addConstraintSql(Connection $connection) + { + $dialect = $connection->getDriver()->schemaDialect(); + + return $dialect->addConstraintSql($this); + } + + /** + * {@inheritDoc} + */ + public function dropConstraintSql(Connection $connection) + { + $dialect = $connection->getDriver()->schemaDialect(); + + return $dialect->dropConstraintSql($this); + } + + /** + * Returns an array of the table schema. + * + * @return array + */ + public function __debugInfo() + { + return [ + 'table' => $this->_table, + 'columns' => $this->_columns, + 'indexes' => $this->_indexes, + 'constraints' => $this->_constraints, + 'options' => $this->_options, + 'typeMap' => $this->_typeMap, + 'temporary' => $this->_temporary, + ]; + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Database\Schema\TableSchema', 'Cake\Database\Schema\Table'); diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/TableSchemaAwareInterface.php b/app/vendor/cakephp/cakephp/src/Database/Schema/TableSchemaAwareInterface.php new file mode 100644 index 000000000..7732b269b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Schema/TableSchemaAwareInterface.php @@ -0,0 +1,37 @@ +_schema = $this->getSchema($connection); + } + + /** + * Build metadata. + * + * @param string|null $name The name of the table to build cache data for. + * @return array Returns a list build table caches + */ + public function build($name = null) + { + $tables = [$name]; + if (empty($name)) { + $tables = $this->_schema->listTables(); + } + + foreach ($tables as $table) { + $this->_schema->describe($table, ['forceRefresh' => true]); + } + + return $tables; + } + + /** + * Clear metadata. + * + * @param string|null $name The name of the table to clear cache data for. + * @return array Returns a list of cleared table caches + */ + public function clear($name = null) + { + $tables = [$name]; + if (empty($name)) { + $tables = $this->_schema->listTables(); + } + $configName = $this->_schema->getCacheMetadata(); + + foreach ($tables as $table) { + $key = $this->_schema->cacheKey($table); + Cache::delete($key, $configName); + } + + return $tables; + } + + /** + * Helper method to get the schema collection. + * + * @param \Cake\Database\Connection $connection Connection object + * @return \Cake\Database\Schema\Collection|\Cake\Database\Schema\CachedCollection + */ + public function getSchema(Connection $connection) + { + if (!method_exists($connection, 'schemaCollection')) { + throw new RuntimeException('The given connection object is not compatible with schema caching, as it does not implement a "schemaCollection()" method.'); + } + + $config = $connection->config(); + if (empty($config['cacheMetadata'])) { + $connection->cacheMetadata(true); + } + + return $connection->getSchemaCollection(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/SqlDialectTrait.php b/app/vendor/cakephp/cakephp/src/Database/SqlDialectTrait.php new file mode 100644 index 000000000..51bac658b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/SqlDialectTrait.php @@ -0,0 +1,288 @@ +_startQuote . $identifier . $this->_endQuote; + } + + // string.string + if (preg_match('/^[\w-]+\.[^ \*]*$/', $identifier)) { + $items = explode('.', $identifier); + + return $this->_startQuote . implode($this->_endQuote . '.' . $this->_startQuote, $items) . $this->_endQuote; + } + + // string.* + if (preg_match('/^[\w-]+\.\*$/', $identifier)) { + return $this->_startQuote . str_replace('.*', $this->_endQuote . '.*', $identifier); + } + + // Functions + if (preg_match('/^([\w-]+)\((.*)\)$/', $identifier, $matches)) { + return $matches[1] . '(' . $this->quoteIdentifier($matches[2]) . ')'; + } + + // Alias.field AS thing + if (preg_match('/^([\w-]+(\.[\w-\s]+|\(.*\))*)\s+AS\s*([\w-]+)$/i', $identifier, $matches)) { + return $this->quoteIdentifier($matches[1]) . ' AS ' . $this->quoteIdentifier($matches[3]); + } + + // string.string with spaces + if (preg_match('/^([\w-]+\.[\w][\w\s\-]*[\w])(.*)/', $identifier, $matches)) { + $items = explode('.', $matches[1]); + $field = implode($this->_endQuote . '.' . $this->_startQuote, $items); + + return $this->_startQuote . $field . $this->_endQuote . $matches[2]; + } + + if (preg_match('/^[\w-_\s]*[\w-_]+/', $identifier)) { + return $this->_startQuote . $identifier . $this->_endQuote; + } + + return $identifier; + } + + /** + * Returns a callable function that will be used to transform a passed Query object. + * This function, in turn, will return an instance of a Query object that has been + * transformed to accommodate any specificities of the SQL dialect in use. + * + * @param string $type the type of query to be transformed + * (select, insert, update, delete) + * @return callable + */ + public function queryTranslator($type) + { + return function ($query) use ($type) { + if ($this->isAutoQuotingEnabled()) { + $query = (new IdentifierQuoter($this))->quote($query); + } + + /** @var \Cake\ORM\Query $query */ + $query = $this->{'_' . $type . 'QueryTranslator'}($query); + $translators = $this->_expressionTranslators(); + if (!$translators) { + return $query; + } + + $query->traverseExpressions(function ($expression) use ($translators, $query) { + foreach ($translators as $class => $method) { + if ($expression instanceof $class) { + $this->{$method}($expression, $query); + } + } + }); + + return $query; + }; + } + + /** + * Returns an associative array of methods that will transform Expression + * objects to conform with the specific SQL dialect. Keys are class names + * and values a method in this class. + * + * @return array + */ + protected function _expressionTranslators() + { + return []; + } + + /** + * Apply translation steps to select queries. + * + * @param \Cake\Database\Query $query The query to translate + * @return \Cake\Database\Query The modified query + */ + protected function _selectQueryTranslator($query) + { + return $this->_transformDistinct($query); + } + + /** + * Returns the passed query after rewriting the DISTINCT clause, so that drivers + * that do not support the "ON" part can provide the actual way it should be done + * + * @param \Cake\Database\Query $query The query to be transformed + * @return \Cake\Database\Query + */ + protected function _transformDistinct($query) + { + if (is_array($query->clause('distinct'))) { + $query->group($query->clause('distinct'), true); + $query->distinct(false); + } + + return $query; + } + + /** + * Apply translation steps to delete queries. + * + * Chops out aliases on delete query conditions as most database dialects do not + * support aliases in delete queries. This also removes aliases + * in table names as they frequently don't work either. + * + * We are intentionally not supporting deletes with joins as they have even poorer support. + * + * @param \Cake\Database\Query $query The query to translate + * @return \Cake\Database\Query The modified query + */ + protected function _deleteQueryTranslator($query) + { + $hadAlias = false; + $tables = []; + foreach ($query->clause('from') as $alias => $table) { + if (is_string($alias)) { + $hadAlias = true; + } + $tables[] = $table; + } + if ($hadAlias) { + $query->from($tables, true); + } + + if (!$hadAlias) { + return $query; + } + + return $this->_removeAliasesFromConditions($query); + } + + /** + * Apply translation steps to update queries. + * + * Chops out aliases on update query conditions as not all database dialects do support + * aliases in update queries. + * + * Just like for delete queries, joins are currently not supported for update queries. + * + * @param \Cake\Database\Query $query The query to translate + * @return \Cake\Database\Query The modified query + */ + protected function _updateQueryTranslator($query) + { + return $this->_removeAliasesFromConditions($query); + } + + /** + * Removes aliases from the `WHERE` clause of a query. + * + * @param \Cake\Database\Query $query The query to process. + * @return \Cake\Database\Query The modified query. + * @throws \RuntimeException In case the processed query contains any joins, as removing + * aliases from the conditions can break references to the joined tables. + */ + protected function _removeAliasesFromConditions($query) + { + if ($query->clause('join')) { + throw new \RuntimeException( + 'Aliases are being removed from conditions for UPDATE/DELETE queries, ' . + 'this can break references to joined tables.' + ); + } + + $conditions = $query->clause('where'); + if ($conditions) { + $conditions->traverse(function ($condition) { + if (!($condition instanceof Comparison)) { + return $condition; + } + + $field = $condition->getField(); + if ($field instanceof ExpressionInterface || strpos($field, '.') === false) { + return $condition; + } + + list(, $field) = explode('.', $field); + $condition->setField($field); + + return $condition; + }); + } + + return $query; + } + + /** + * Apply translation steps to insert queries. + * + * @param \Cake\Database\Query $query The query to translate + * @return \Cake\Database\Query The modified query + */ + protected function _insertQueryTranslator($query) + { + return $query; + } + + /** + * Returns a SQL snippet for creating a new transaction savepoint + * + * @param string $name save point name + * @return string + */ + public function savePointSQL($name) + { + return 'SAVEPOINT LEVEL' . $name; + } + + /** + * Returns a SQL snippet for releasing a previously created save point + * + * @param string $name save point name + * @return string + */ + public function releaseSavePointSQL($name) + { + return 'RELEASE SAVEPOINT LEVEL' . $name; + } + + /** + * Returns a SQL snippet for rollbacking a previously created save point + * + * @param string $name save point name + * @return string + */ + public function rollbackSavePointSQL($name) + { + return 'ROLLBACK TO SAVEPOINT LEVEL' . $name; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/SqliteCompiler.php b/app/vendor/cakephp/cakephp/src/Database/SqliteCompiler.php new file mode 100644 index 000000000..5a9e54767 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/SqliteCompiler.php @@ -0,0 +1,32 @@ + 'DELETE', + 'where' => ' WHERE %s', + 'group' => ' GROUP BY %s ', + 'having' => ' HAVING %s ', + 'order' => ' %s', + 'offset' => ' OFFSET %s ROWS', + 'epilog' => ' %s' + ]; + + /** + * {@inheritDoc} + */ + protected $_selectParts = [ + 'select', 'from', 'join', 'where', 'group', 'having', 'order', 'offset', + 'limit', 'union', 'epilog' + ]; + + /** + * Generates the INSERT part of a SQL query + * + * To better handle concurrency and low transaction isolation levels, + * we also include an OUTPUT clause so we can ensure we get the inserted + * row's data back. + * + * @param array $parts The parts to build + * @param \Cake\Database\Query $query The query that is being compiled + * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions + * @return string + */ + protected function _buildInsertPart($parts, $query, $generator) + { + $table = $parts[0]; + $columns = $this->_stringifyExpressions($parts[1], $generator); + $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $generator); + + return sprintf( + 'INSERT%s INTO %s (%s) OUTPUT INSERTED.*', + $modifiers, + $table, + implode(', ', $columns) + ); + } + + /** + * Generates the LIMIT part of a SQL query + * + * @param int $limit the limit clause + * @param \Cake\Database\Query $query The query that is being compiled + * @return string + */ + protected function _buildLimitPart($limit, $query) + { + if ($limit === null || $query->clause('offset') === null) { + return ''; + } + + return sprintf(' FETCH FIRST %d ROWS ONLY', $limit); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/BufferResultsTrait.php b/app/vendor/cakephp/cakephp/src/Database/Statement/BufferResultsTrait.php new file mode 100644 index 000000000..0ad4c795f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Statement/BufferResultsTrait.php @@ -0,0 +1,44 @@ +_bufferResults = (bool)$buffer; + + return $this; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/BufferedStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/BufferedStatement.php new file mode 100644 index 000000000..9fd6b5e38 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Statement/BufferedStatement.php @@ -0,0 +1,161 @@ +_reset(); + + return parent::execute($params); + } + + /** + * {@inheritDoc} + * + * @param string $type The type to fetch. + * @return array|false + */ + public function fetch($type = parent::FETCH_TYPE_NUM) + { + if ($this->_allFetched) { + $row = ($this->_counter < $this->_count) ? $this->_records[$this->_counter++] : false; + $row = ($row && $type === static::FETCH_TYPE_NUM) ? array_values($row) : $row; + + return $row; + } + + $record = parent::fetch($type); + + if ($record === false) { + $this->_allFetched = true; + $this->_counter = $this->_count + 1; + $this->_statement->closeCursor(); + + return false; + } + + $this->_count++; + + return $this->_records[] = $record; + } + + /** + * {@inheritdoc} + */ + public function fetchAssoc() + { + return $this->fetch(static::FETCH_TYPE_ASSOC); + } + + /** + * {@inheritDoc} + * + * @param string $type The type to fetch. + * @return array + */ + public function fetchAll($type = parent::FETCH_TYPE_NUM) + { + if ($this->_allFetched) { + return $this->_records; + } + + $this->_records = parent::fetchAll($type); + $this->_count = count($this->_records); + $this->_allFetched = true; + $this->_statement->closeCursor(); + + return $this->_records; + } + + /** + * {@inheritDoc} + */ + public function rowCount() + { + if (!$this->_allFetched) { + $counter = $this->_counter; + while ($this->fetch(static::FETCH_TYPE_ASSOC)) { + } + $this->_counter = $counter; + } + + return $this->_count; + } + + /** + * Rewind the _counter property + * + * @return void + */ + public function rewind() + { + $this->_counter = 0; + } + + /** + * Reset all properties + * + * @return void + */ + protected function _reset() + { + $this->_count = $this->_counter = 0; + $this->_records = []; + $this->_allFetched = false; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/CallbackStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/CallbackStatement.php new file mode 100644 index 000000000..b7035ff1e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Statement/CallbackStatement.php @@ -0,0 +1,74 @@ +_callback = $callback; + } + + /** + * Fetch a row from the statement. + * + * The result will be processed by the callback when it is not `false`. + * + * @param string $type Either 'num' or 'assoc' to indicate the result format you would like. + * @return array|false + */ + public function fetch($type = parent::FETCH_TYPE_NUM) + { + $callback = $this->_callback; + $row = $this->_statement->fetch($type); + + return $row === false ? $row : $callback($row); + } + + /** + * Fetch all rows from the statement. + * + * Each row in the result will be processed by the callback when it is not `false. + * + * @param string $type Either 'num' or 'assoc' to indicate the result format you would like. + * @return array + */ + public function fetchAll($type = parent::FETCH_TYPE_NUM) + { + return array_map($this->_callback, $this->_statement->fetchAll($type)); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/MysqlStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/MysqlStatement.php new file mode 100644 index 000000000..0f751945b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Statement/MysqlStatement.php @@ -0,0 +1,46 @@ +_driver->getConnection(); + + try { + $connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $this->_bufferResults); + $result = $this->_statement->execute($params); + } finally { + $connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); + } + + return $result; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/PDOStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/PDOStatement.php new file mode 100644 index 000000000..4bf00f928 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Statement/PDOStatement.php @@ -0,0 +1,134 @@ +bindValue(1, 'a title'); + * $statement->bindValue(2, 5, PDO::INT); + * $statement->bindValue('active', true, 'boolean'); + * $statement->bindValue(5, new \DateTime(), 'date'); + * ``` + * + * @param string|int $column name or param position to be bound + * @param mixed $value The value to bind to variable in query + * @param string|int $type PDO type or name of configured Type class + * @return void + */ + public function bindValue($column, $value, $type = 'string') + { + if ($type === null) { + $type = 'string'; + } + if (!ctype_digit($type)) { + list($value, $type) = $this->cast($value, $type); + } + $this->_statement->bindValue($column, $value, $type); + } + + /** + * Returns the next row for the result set after executing this statement. + * Rows can be fetched to contain columns as names or positions. If no + * rows are left in result set, this method will return false + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title'] + * ``` + * + * @param string $type 'num' for positional columns, assoc for named columns + * @return array|false Result array containing columns and values or false if no results + * are left + */ + public function fetch($type = parent::FETCH_TYPE_NUM) + { + if ($type === static::FETCH_TYPE_NUM) { + return $this->_statement->fetch(PDO::FETCH_NUM); + } + if ($type === static::FETCH_TYPE_ASSOC) { + return $this->_statement->fetch(PDO::FETCH_ASSOC); + } + if ($type === static::FETCH_TYPE_OBJ) { + return $this->_statement->fetch(PDO::FETCH_OBJ); + } + + return $this->_statement->fetch($type); + } + + /** + * Returns an array with all rows resulting from executing this statement + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']] + * ``` + * + * @param string $type num for fetching columns as positional keys or assoc for column names as keys + * @return array list of all results from database for this statement + */ + public function fetchAll($type = parent::FETCH_TYPE_NUM) + { + if ($type === static::FETCH_TYPE_NUM) { + return $this->_statement->fetchAll(PDO::FETCH_NUM); + } + if ($type === static::FETCH_TYPE_ASSOC) { + return $this->_statement->fetchAll(PDO::FETCH_ASSOC); + } + if ($type === static::FETCH_TYPE_OBJ) { + return $this->_statement->fetchAll(PDO::FETCH_OBJ); + } + + return $this->_statement->fetchAll($type); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/SqliteStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/SqliteStatement.php new file mode 100644 index 000000000..08e0b558b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Statement/SqliteStatement.php @@ -0,0 +1,62 @@ +_statement instanceof BufferedStatement) { + $this->_statement = $this->_statement->getInnerStatement(); + } + + if ($this->_bufferResults) { + $this->_statement = new BufferedStatement($this->_statement, $this->_driver); + } + + return $this->_statement->execute($params); + } + + /** + * Returns the number of rows returned of affected by last execution + * + * @return int + */ + public function rowCount() + { + if (preg_match('/^(?:DELETE|UPDATE|INSERT)/i', $this->_statement->queryString)) { + $changes = $this->_driver->prepare('SELECT CHANGES()'); + $changes->execute(); + $count = $changes->fetch()[0]; + $changes->closeCursor(); + + return (int)$count; + } + + return parent::rowCount(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/SqlserverStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/SqlserverStatement.php new file mode 100644 index 000000000..5b1eb539d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Statement/SqlserverStatement.php @@ -0,0 +1,47 @@ +cast($value, $type); + } + if ($type == PDO::PARAM_LOB) { + $this->_statement->bindParam($column, $value, $type, 0, PDO::SQLSRV_ENCODING_BINARY); + } else { + $this->_statement->bindValue($column, $value, $type); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/StatementDecorator.php b/app/vendor/cakephp/cakephp/src/Database/Statement/StatementDecorator.php new file mode 100644 index 000000000..bd9d29b0b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Statement/StatementDecorator.php @@ -0,0 +1,375 @@ +_statement = $statement; + $this->_driver = $driver; + } + + /** + * Magic getter to return $queryString as read-only. + * + * @param string $property internal property to get + * @return mixed + */ + public function __get($property) + { + if ($property === 'queryString') { + return $this->_statement->queryString; + } + } + + /** + * Assign a value to a positional or named variable in prepared query. If using + * positional variables you need to start with index one, if using named params then + * just use the name in any order. + * + * It is not allowed to combine positional and named variables in the same statement. + * + * ### Examples: + * + * ``` + * $statement->bindValue(1, 'a title'); + * $statement->bindValue('active', true, 'boolean'); + * $statement->bindValue(5, new \DateTime(), 'date'); + * ``` + * + * @param string|int $column name or param position to be bound + * @param mixed $value The value to bind to variable in query + * @param string $type name of configured Type class + * @return void + */ + public function bindValue($column, $value, $type = 'string') + { + $this->_statement->bindValue($column, $value, $type); + } + + /** + * Closes a cursor in the database, freeing up any resources and memory + * allocated to it. In most cases you don't need to call this method, as it is + * automatically called after fetching all results from the result set. + * + * @return void + */ + public function closeCursor() + { + $this->_statement->closeCursor(); + } + + /** + * Returns the number of columns this statement's results will contain. + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * echo $statement->columnCount(); // outputs 2 + * ``` + * + * @return int + */ + public function columnCount() + { + return $this->_statement->columnCount(); + } + + /** + * Returns the error code for the last error that occurred when executing this statement. + * + * @return int|string + */ + public function errorCode() + { + return $this->_statement->errorCode(); + } + + /** + * Returns the error information for the last error that occurred when executing + * this statement. + * + * @return array + */ + public function errorInfo() + { + return $this->_statement->errorInfo(); + } + + /** + * Executes the statement by sending the SQL query to the database. It can optionally + * take an array or arguments to be bound to the query variables. Please note + * that binding parameters from this method will not perform any custom type conversion + * as it would normally happen when calling `bindValue`. + * + * @param array|null $params list of values to be bound to query + * @return bool true on success, false otherwise + */ + public function execute($params = null) + { + $this->_hasExecuted = true; + + return $this->_statement->execute($params); + } + + /** + * Returns the next row for the result set after executing this statement. + * Rows can be fetched to contain columns as names or positions. If no + * rows are left in result set, this method will return false. + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title'] + * ``` + * + * @param string $type 'num' for positional columns, assoc for named columns + * @return array|false Result array containing columns and values or false if no results + * are left + */ + public function fetch($type = self::FETCH_TYPE_NUM) + { + return $this->_statement->fetch($type); + } + + /** + * Returns the next row in a result set as an associative array. Calling this function is the same as calling + * $statement->fetch(StatementDecorator::FETCH_TYPE_ASSOC). If no results are found false is returned. + * + * @return array|false Result array containing columns and values an an associative array or false if no results + */ + public function fetchAssoc() + { + return $this->fetch(static::FETCH_TYPE_ASSOC); + } + + /** + * Returns the value of the result at position. + * + * @param int $position The numeric position of the column to retrieve in the result + * @return mixed|false Returns the specific value of the column designated at $position + */ + public function fetchColumn($position) + { + $result = $this->fetch(static::FETCH_TYPE_NUM); + if (isset($result[$position])) { + return $result[$position]; + } + + return false; + } + + /** + * Returns an array with all rows resulting from executing this statement. + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']] + * ``` + * + * @param string $type num for fetching columns as positional keys or assoc for column names as keys + * @return array List of all results from database for this statement + */ + public function fetchAll($type = self::FETCH_TYPE_NUM) + { + return $this->_statement->fetchAll($type); + } + + /** + * Returns the number of rows affected by this SQL statement. + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->rowCount()); // will show 1 + * ``` + * + * @return int + */ + public function rowCount() + { + return $this->_statement->rowCount(); + } + + /** + * Statements are iterable as arrays, this method will return + * the iterator object for traversing all items in the result. + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * foreach ($statement as $row) { + * //do stuff + * } + * ``` + * + * @return \Cake\Database\StatementInterface|\PDOStatement + */ + public function getIterator() + { + if (!$this->_hasExecuted) { + $this->execute(); + } + + return $this->_statement; + } + + /** + * Statements can be passed as argument for count() to return the number + * for affected rows from last execution. + * + * @return int + */ + public function count() + { + return $this->rowCount(); + } + + /** + * Binds a set of values to statement object with corresponding type. + * + * @param array $params list of values to be bound + * @param array $types list of types to be used, keys should match those in $params + * @return void + */ + public function bind($params, $types) + { + if (empty($params)) { + return; + } + + $anonymousParams = is_int(key($params)) ? true : false; + $offset = 1; + foreach ($params as $index => $value) { + $type = null; + if (isset($types[$index])) { + $type = $types[$index]; + } + if ($anonymousParams) { + $index += $offset; + } + $this->bindValue($index, $value, $type); + } + } + + /** + * Returns the latest primary inserted using this statement. + * + * @param string|null $table table name or sequence to get last insert value from + * @param string|null $column the name of the column representing the primary key + * @return string|int + */ + public function lastInsertId($table = null, $column = null) + { + $row = null; + if ($column && $this->columnCount()) { + $row = $this->fetch(static::FETCH_TYPE_ASSOC); + } + if (isset($row[$column])) { + return $row[$column]; + } + + return $this->_driver->lastInsertId($table, $column); + } + + /** + * Returns the statement object that was decorated by this class. + * + * @return \Cake\Database\StatementInterface|\PDOStatement + */ + public function getInnerStatement() + { + return $this->_statement; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/StatementInterface.php b/app/vendor/cakephp/cakephp/src/Database/StatementInterface.php new file mode 100644 index 000000000..f88ef22de --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/StatementInterface.php @@ -0,0 +1,171 @@ +bindValue(1, 'a title'); + * $statement->bindValue('active', true, 'boolean'); + * $statement->bindValue(5, new \DateTime(), 'date'); + * ``` + * + * @param string|int $column name or param position to be bound + * @param mixed $value The value to bind to variable in query + * @param string $type name of configured Type class + * @return void + */ + public function bindValue($column, $value, $type = 'string'); + + /** + * Closes a cursor in the database, freeing up any resources and memory + * allocated to it. In most cases you don't need to call this method, as it is + * automatically called after fetching all results from the result set. + * + * @return void + */ + public function closeCursor(); + + /** + * Returns the number of columns this statement's results will contain + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * echo $statement->columnCount(); // outputs 2 + * ``` + * + * @return int + */ + public function columnCount(); + + /** + * Returns the error code for the last error that occurred when executing this statement + * + * @return int|string + */ + public function errorCode(); + + /** + * Returns the error information for the last error that occurred when executing + * this statement + * + * @return array + */ + public function errorInfo(); + + /** + * Executes the statement by sending the SQL query to the database. It can optionally + * take an array or arguments to be bound to the query variables. Please note + * that binding parameters from this method will not perform any custom type conversion + * as it would normally happen when calling `bindValue` + * + * @param array|null $params list of values to be bound to query + * @return bool true on success, false otherwise + */ + public function execute($params = null); + + /** + * Returns the next row for the result set after executing this statement. + * Rows can be fetched to contain columns as names or positions. If no + * rows are left in result set, this method will return false + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title'] + * ``` + * + * @param string $type 'num' for positional columns, assoc for named columns + * @return array|false Result array containing columns and values or false if no results + * are left + */ + public function fetch($type = 'num'); + + /** + * Returns an array with all rows resulting from executing this statement + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']] + * ``` + * + * @param string $type num for fetching columns as positional keys or assoc for column names as keys + * @return array list of all results from database for this statement + */ + public function fetchAll($type = 'num'); + + /** + * Returns the number of rows affected by this SQL statement + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->rowCount()); // will show 1 + * ``` + * + * @return int + */ + public function rowCount(); + + /** + * Statements can be passed as argument for count() + * to return the number for affected rows from last execution + * + * @return int + */ + public function count(); + + /** + * Binds a set of values to statement object with corresponding type + * + * @param array $params list of values to be bound + * @param array $types list of types to be used, keys should match those in $params + * @return void + */ + public function bind($params, $types); + + /** + * Returns the latest primary inserted using this statement + * + * @param string|null $table table name or sequence to get last insert value from + * @param string|null $column the name of the column representing the primary key + * @return string + */ + public function lastInsertId($table = null, $column = null); +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type.php b/app/vendor/cakephp/cakephp/src/Database/Type.php new file mode 100644 index 000000000..ad566e8c2 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type.php @@ -0,0 +1,381 @@ + 'Cake\Database\Type\IntegerType', + 'smallinteger' => 'Cake\Database\Type\IntegerType', + 'integer' => 'Cake\Database\Type\IntegerType', + 'biginteger' => 'Cake\Database\Type\IntegerType', + 'binary' => 'Cake\Database\Type\BinaryType', + 'binaryuuid' => 'Cake\Database\Type\BinaryUuidType', + 'boolean' => 'Cake\Database\Type\BoolType', + 'date' => 'Cake\Database\Type\DateType', + 'datetime' => 'Cake\Database\Type\DateTimeType', + 'decimal' => 'Cake\Database\Type\DecimalType', + 'float' => 'Cake\Database\Type\FloatType', + 'json' => 'Cake\Database\Type\JsonType', + 'string' => 'Cake\Database\Type\StringType', + 'text' => 'Cake\Database\Type\StringType', + 'time' => 'Cake\Database\Type\TimeType', + 'timestamp' => 'Cake\Database\Type\DateTimeType', + 'uuid' => 'Cake\Database\Type\UuidType', + ]; + + /** + * List of basic type mappings, used to avoid having to instantiate a class + * for doing conversion on these. + * + * @var array + * @deprecated 3.1 All types will now use a specific class + */ + protected static $_basicTypes = [ + 'string' => ['callback' => [Type::class, 'strval']], + 'text' => ['callback' => [Type::class, 'strval']], + 'boolean' => [ + 'callback' => [Type::class, 'boolval'], + 'pdo' => PDO::PARAM_BOOL + ], + ]; + + /** + * Contains a map of type object instances to be reused if needed. + * + * @var \Cake\Database\Type[] + */ + protected static $_builtTypes = []; + + /** + * Identifier name for this type + * + * @var string|null + */ + protected $_name; + + /** + * Constructor + * + * @param string|null $name The name identifying this type + */ + public function __construct($name = null) + { + $this->_name = $name; + } + + /** + * Returns a Type object capable of converting a type identified by name. + * + * @param string $name type identifier + * @throws \InvalidArgumentException If type identifier is unknown + * @return \Cake\Database\Type + */ + public static function build($name) + { + if (isset(static::$_builtTypes[$name])) { + return static::$_builtTypes[$name]; + } + if (!isset(static::$_types[$name])) { + throw new InvalidArgumentException(sprintf('Unknown type "%s"', $name)); + } + if (is_string(static::$_types[$name])) { + return static::$_builtTypes[$name] = new static::$_types[$name]($name); + } + + return static::$_builtTypes[$name] = static::$_types[$name]; + } + + /** + * Returns an arrays with all the mapped type objects, indexed by name. + * + * @return array + */ + public static function buildAll() + { + $result = []; + foreach (static::$_types as $name => $type) { + $result[$name] = isset(static::$_builtTypes[$name]) ? static::$_builtTypes[$name] : static::build($name); + } + + return $result; + } + + /** + * Returns a Type object capable of converting a type identified by $name + * + * @param string $name The type identifier you want to set. + * @param \Cake\Database\Type $instance The type instance you want to set. + * @return void + */ + public static function set($name, Type $instance) + { + static::$_builtTypes[$name] = $instance; + } + + /** + * Registers a new type identifier and maps it to a fully namespaced classname, + * If called with no arguments it will return current types map array + * If $className is omitted it will return mapped class for $type + * + * Deprecated 3.6.2: + * - The usage of $type as string[]|\Cake\Database\Type[] is deprecated. + * Use Type::setMap() with string[] instead. + * - Passing $className as \Cake\Database\Type instance is deprecated, use + * class name string only. + * - Using this method as getter is deprecated. Use Type::getMap() instead. + * + * @param string|string[]|\Cake\Database\Type[]|null $type If string name of type to map, if array list of arrays to be mapped + * @param string|\Cake\Database\Type|null $className The classname or object instance of it to register. + * @return array|string|null If $type is null then array with current map, if $className is null string + * configured class name for give $type, null otherwise + */ + public static function map($type = null, $className = null) + { + if ($type === null) { + deprecationWarning( + 'Using `Type::map()` as getter is deprecated. ' . + 'Use `Type::getMap()` instead.' + ); + + return static::$_types; + } + if (is_array($type)) { + deprecationWarning( + 'Using `Type::map()` to set complete types map is deprecated. ' . + 'Use `Type::setMap()` instead.' + ); + + static::$_types = $type; + + return null; + } + if ($className === null) { + deprecationWarning( + 'Using `Type::map()` as getter is deprecated. ' . + 'Use `Type::getMap()` instead.' + ); + + return isset(static::$_types[$type]) ? static::$_types[$type] : null; + } + + if (!is_string($className)) { + deprecationWarning( + 'Passing $className as object to Type::map() is deprecated. ' . + 'Use Type::set() instead.' + ); + } + + static::$_types[$type] = $className; + unset(static::$_builtTypes[$type]); + } + + /** + * Set type to classname mapping. + * + * @param string[] $map List of types to be mapped. + * @return void + * @since 3.6.2 + */ + public static function setMap(array $map) + { + static::$_types = $map; + static::$_builtTypes = []; + } + + /** + * Get mapped class name or instance for type(s). + * + * @param string|null $type Type name to get mapped class for or null to get map array. + * @return array|string|\Cake\Database\TypeInterface|null Configured class name or instance for give $type or map array. + * @since 3.6.2 + */ + public static function getMap($type = null) + { + if ($type === null) { + return static::$_types; + } + + return isset(static::$_types[$type]) ? static::$_types[$type] : null; + } + + /** + * Clears out all created instances and mapped types classes, useful for testing + * + * @return void + */ + public static function clear() + { + static::$_types = []; + static::$_builtTypes = []; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->_name; + } + + /** + * {@inheritDoc} + */ + public function getBaseType() + { + return $this->_name; + } + + /** + * {@inheritDoc} + */ + public function toDatabase($value, Driver $driver) + { + return $this->_basicTypeCast($value); + } + + /** + * Casts given value from a database type to PHP equivalent + * + * @param mixed $value Value to be converted to PHP equivalent + * @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted + * @return mixed + */ + public function toPHP($value, Driver $driver) + { + return $this->_basicTypeCast($value); + } + + /** + * Checks whether this type is a basic one and can be converted using a callback + * If it is, returns converted value + * + * @param mixed $value Value to be converted to PHP equivalent + * @return mixed + * @deprecated 3.1 All types should now be a specific class + */ + protected function _basicTypeCast($value) + { + deprecationWarning( + 'Using Type::_basicTypeCast() is deprecated. ' . + "The '{$this->_name}' type needs to be updated to implement `TypeInterface`." + ); + if ($value === null) { + return null; + } + if (!empty(static::$_basicTypes[$this->_name])) { + $typeInfo = static::$_basicTypes[$this->_name]; + if (isset($typeInfo['callback'])) { + return $typeInfo['callback']($value); + } + } + + return $value; + } + + /** + * {@inheritDoc} + */ + public function toStatement($value, Driver $driver) + { + if ($value === null) { + return PDO::PARAM_NULL; + } + + return PDO::PARAM_STR; + } + + /** + * Type converter for boolean values. + * + * Will convert string true/false into booleans. + * + * @param mixed $value The value to convert to a boolean. + * @return bool + * @deprecated 3.1.8 This method is now unused. + */ + public static function boolval($value) + { + deprecationWarning('Type::boolval() is deprecated.'); + if (is_string($value) && !is_numeric($value)) { + return strtolower($value) === 'true'; + } + + return !empty($value); + } + + /** + * Type converter for string values. + * + * Will convert values into strings + * + * @param mixed $value The value to convert to a string. + * @return string + * @deprecated 3.1.8 This method is now unused. + */ + public static function strval($value) + { + deprecationWarning('Type::strval() is deprecated.'); + if (is_array($value)) { + $value = ''; + } + + return (string)$value; + } + + /** + * {@inheritDoc} + */ + public function newId() + { + return null; + } + + /** + * {@inheritDoc} + */ + public function marshal($value) + { + return $this->_basicTypeCast($value); + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + return [ + 'name' => $this->_name, + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/BatchCastingInterface.php b/app/vendor/cakephp/cakephp/src/Database/Type/BatchCastingInterface.php new file mode 100644 index 000000000..0424d9cbc --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/BatchCastingInterface.php @@ -0,0 +1,36 @@ +_name = $name; + } + + /** + * Convert binary data into the database format. + * + * Binary data is not altered before being inserted into the database. + * As PDO will handle reading file handles. + * + * @param string|resource $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return string|resource + */ + public function toDatabase($value, Driver $driver) + { + return $value; + } + + /** + * Convert binary into resource handles + * + * @param null|string|resource $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return resource|null + * @throws \Cake\Core\Exception\Exception + */ + public function toPHP($value, Driver $driver) + { + if ($value === null) { + return null; + } + if (is_string($value) && $driver instanceof Sqlserver) { + $value = pack('H*', $value); + } + if (is_string($value)) { + return fopen('data:text/plain;base64,' . base64_encode($value), 'rb'); + } + if (is_resource($value)) { + return $value; + } + throw new Exception(sprintf('Unable to convert %s into binary.', gettype($value))); + } + + /** + * Get the correct PDO binding type for Binary data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\Driver $driver The driver. + * @return int + */ + public function toStatement($value, Driver $driver) + { + return PDO::PARAM_LOB; + } + + /** + * Marshalls flat data into PHP objects. + * + * Most useful for converting request data into PHP objects + * that make sense for the rest of the ORM/Database layers. + * + * @param mixed $value The value to convert. + * + * @return mixed Converted value. + */ + public function marshal($value) + { + return $value; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/BinaryUuidType.php b/app/vendor/cakephp/cakephp/src/Database/Type/BinaryUuidType.php new file mode 100644 index 000000000..47b88aa7f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/BinaryUuidType.php @@ -0,0 +1,168 @@ +_name = $name; + } + + /** + * Convert binary uuid data into the database format. + * + * Binary data is not altered before being inserted into the database. + * As PDO will handle reading file handles. + * + * @param string|resource $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return string|resource + */ + public function toDatabase($value, Driver $driver) + { + if (is_string($value)) { + return $this->convertStringToBinaryUuid($value); + } + + return $value; + } + + /** + * Generate a new binary UUID + * + * @return string A new primary key value. + */ + public function newId() + { + return Text::uuid(); + } + + /** + * Convert binary uuid into resource handles + * + * @param null|string|resource $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return resource|string|null + * @throws \Cake\Core\Exception\Exception + */ + public function toPHP($value, Driver $driver) + { + if ($value === null) { + return null; + } + if (is_string($value)) { + return $this->convertBinaryUuidToString($value); + } + if (is_resource($value)) { + return $value; + } + + throw new Exception(sprintf('Unable to convert %s into binary uuid.', gettype($value))); + } + + /** + * Get the correct PDO binding type for Binary data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\Driver $driver The driver. + * @return int + */ + public function toStatement($value, Driver $driver) + { + return PDO::PARAM_LOB; + } + + /** + * Marshalls flat data into PHP objects. + * + * Most useful for converting request data into PHP objects + * that make sense for the rest of the ORM/Database layers. + * + * @param mixed $value The value to convert. + * + * @return mixed Converted value. + */ + public function marshal($value) + { + return $value; + } + + /** + * Converts a binary uuid to a string representation + * + * + * @param mixed $binary The value to convert. + * + * @return string Converted value. + */ + protected function convertBinaryUuidToString($binary) + { + $string = unpack("H*", $binary); + + $string = preg_replace( + "/([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})/", + "$1-$2-$3-$4-$5", + $string + ); + + return $string[1]; + } + + /** + * Converts a string uuid to a binary representation + * + * + * @param mixed $string The value to convert. + * + * @return mixed Converted value. + */ + protected function convertStringToBinaryUuid($string) + { + $string = str_replace('-', '', $string); + + return pack("H*", $string); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/BoolType.php b/app/vendor/cakephp/cakephp/src/Database/Type/BoolType.php new file mode 100644 index 000000000..8c95c5f66 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/BoolType.php @@ -0,0 +1,167 @@ +_name = $name; + } + + /** + * Convert bool data into the database format. + * + * @param mixed $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return bool|null + */ + public function toDatabase($value, Driver $driver) + { + if ($value === true || $value === false || $value === null) { + return $value; + } + + if (in_array($value, [1, 0, '1', '0'], true)) { + return (bool)$value; + } + + throw new InvalidArgumentException(sprintf( + 'Cannot convert value of type `%s` to bool', + getTypeName($value) + )); + } + + /** + * Convert bool values to PHP booleans + * + * @param mixed $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return bool|null + */ + public function toPHP($value, Driver $driver) + { + if ($value === null || $value === true || $value === false) { + return $value; + } + + if (!is_numeric($value)) { + return strtolower($value) === 'true'; + } + + return !empty($value); + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function manyToPHP(array $values, array $fields, Driver $driver) + { + foreach ($fields as $field) { + if (!isset($values[$field]) || $values[$field] === true || $values[$field] === false) { + continue; + } + + if ($values[$field] === '1') { + $values[$field] = true; + continue; + } + + if ($values[$field] === '0') { + $values[$field] = false; + continue; + } + + $value = $values[$field]; + if (!is_numeric($value)) { + $values[$field] = strtolower($value) === 'true'; + continue; + } + + $values[$field] = !empty($value); + } + + return $values; + } + + /** + * Get the correct PDO binding type for bool data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\Driver $driver The driver. + * @return int + */ + public function toStatement($value, Driver $driver) + { + if ($value === null) { + return PDO::PARAM_NULL; + } + + return PDO::PARAM_BOOL; + } + + /** + * Marshalls request data into PHP booleans. + * + * @param mixed $value The value to convert. + * @return bool|null Converted value. + */ + public function marshal($value) + { + if ($value === null) { + return null; + } + if ($value === 'true') { + return true; + } + if ($value === 'false') { + return false; + } + + return !empty($value); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/DateTimeType.php b/app/vendor/cakephp/cakephp/src/Database/Type/DateTimeType.php new file mode 100644 index 000000000..949fefa54 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/DateTimeType.php @@ -0,0 +1,427 @@ +_name = $name; + + $this->_setClassName(static::$dateTimeClass, 'DateTime'); + } + + /** + * Convert DateTime instance into strings. + * + * @param string|int|\DateTime|\DateTimeImmutable $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return string|null + */ + public function toDatabase($value, Driver $driver) + { + if ($value === null || is_string($value)) { + return $value; + } + if (is_int($value)) { + $class = $this->_className; + $value = new $class('@' . $value); + } + + $format = (array)$this->_format; + + if ($this->dbTimezone !== null + && $this->dbTimezone->getName() !== $value->getTimezone()->getName() + ) { + if (!$value instanceof DateTimeImmutable) { + $value = clone $value; + } + $value = $value->setTimezone($this->dbTimezone); + } + + return $value->format(array_shift($format)); + } + + /** + * Set database timezone. + * + * Specified timezone will be set for DateTime objects before generating + * datetime string for saving to database. If `null` no timezone conversion + * will be done. + * + * @param string|\DateTimeZone|null $timezone Database timezone. + * @return $this + */ + public function setTimezone($timezone) + { + if (is_string($timezone)) { + $timezone = new DateTimeZone($timezone); + } + $this->dbTimezone = $timezone; + + return $this; + } + + /** + * Convert strings into DateTime instances. + * + * @param string $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return \Cake\I18n\Time|\DateTime|null + */ + public function toPHP($value, Driver $driver) + { + if ($value === null || strpos($value, '0000-00-00') === 0) { + return null; + } + + $instance = clone $this->_datetimeInstance; + $instance = $instance->modify($value); + + if ($this->setToDateStart) { + $instance = $instance->setTime(0, 0, 0); + } + + return $instance; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function manyToPHP(array $values, array $fields, Driver $driver) + { + foreach ($fields as $field) { + if (!isset($values[$field])) { + continue; + } + + if (strpos($values[$field], '0000-00-00') === 0) { + $values[$field] = null; + continue; + } + + $instance = clone $this->_datetimeInstance; + $instance = $instance->modify($values[$field]); + + if ($this->setToDateStart) { + $instance = $instance->setTime(0, 0, 0); + } + + $values[$field] = $instance; + } + + return $values; + } + + /** + * Convert request data into a datetime object. + * + * @param mixed $value Request data + * @return \DateTimeInterface|null + */ + public function marshal($value) + { + if ($value instanceof DateTimeInterface) { + return $value; + } + + $class = $this->_className; + try { + $compare = $date = false; + if ($value === '' || $value === null || $value === false || $value === true) { + return null; + } + $isString = is_string($value); + if (ctype_digit($value)) { + $date = new $class('@' . $value); + } elseif ($isString && $this->_useLocaleParser) { + return $this->_parseValue($value); + } elseif ($isString) { + $date = new $class($value); + $compare = true; + } + if ($compare && $date && !$this->_compare($date, $value)) { + return $value; + } + if ($date) { + return $date; + } + } catch (Exception $e) { + return $value; + } + + if (is_array($value) && implode('', $value) === '') { + return null; + } + $value += ['hour' => 0, 'minute' => 0, 'second' => 0]; + + $format = ''; + if (isset($value['year'], $value['month'], $value['day']) && + (is_numeric($value['year']) && is_numeric($value['month']) && is_numeric($value['day'])) + ) { + $format .= sprintf('%d-%02d-%02d', $value['year'], $value['month'], $value['day']); + } + + if (isset($value['meridian']) && (int)$value['hour'] === 12) { + $value['hour'] = 0; + } + if (isset($value['meridian'])) { + $value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12; + } + $format .= sprintf( + '%s%02d:%02d:%02d', + empty($format) ? '' : ' ', + $value['hour'], + $value['minute'], + $value['second'] + ); + $tz = isset($value['timezone']) ? $value['timezone'] : null; + + return new $class($format, $tz); + } + + /** + * @param \Cake\I18n\Time|\DateTime $date DateTime object + * @param mixed $value Request data + * @return bool + */ + protected function _compare($date, $value) + { + foreach ((array)$this->_format as $format) { + if ($date->format($format) === $value) { + return true; + } + } + + return false; + } + + /** + * Sets whether or not to parse dates passed to the marshal() function + * by using a locale aware parser. + * + * @param bool $enable Whether or not to enable + * @return $this + */ + public function useLocaleParser($enable = true) + { + if ($enable === false) { + $this->_useLocaleParser = $enable; + + return $this; + } + if (method_exists($this->_className, 'parseDateTime')) { + $this->_useLocaleParser = $enable; + + return $this; + } + throw new RuntimeException( + sprintf('Cannot use locale parsing with the %s class', $this->_className) + ); + } + + /** + * Sets the format string to use for parsing dates in this class. The formats + * that are accepted are documented in the `Cake\I18n\Time::parseDateTime()` + * function. + * + * @param string|array $format The format in which the string are passed. + * @see \Cake\I18n\Time::parseDateTime() + * @return $this + */ + public function setLocaleFormat($format) + { + $this->_localeFormat = $format; + + return $this; + } + + /** + * Change the preferred class name to the FrozenTime implementation. + * + * @return $this + */ + public function useImmutable() + { + $this->_setClassName('Cake\I18n\FrozenTime', 'DateTimeImmutable'); + + return $this; + } + + /** + * Set the classname to use when building objects. + * + * @param string $class The classname to use. + * @param string $fallback The classname to use when the preferred class does not exist. + * @return void + */ + protected function _setClassName($class, $fallback) + { + if (!class_exists($class)) { + $class = $fallback; + } + $this->_className = $class; + $this->_datetimeInstance = new $this->_className; + } + + /** + * Get the classname used for building objects. + * + * @return string + */ + public function getDateTimeClassName() + { + return $this->_className; + } + + /** + * Change the preferred class name to the mutable Time implementation. + * + * @return $this + */ + public function useMutable() + { + $this->_setClassName('Cake\I18n\Time', 'DateTime'); + + return $this; + } + + /** + * Converts a string into a DateTime object after parsing it using the locale + * aware parser with the specified format. + * + * @param string $value The value to parse and convert to an object. + * @return \Cake\I18n\Time|null + */ + protected function _parseValue($value) + { + /* @var \Cake\I18n\Time $class */ + $class = $this->_className; + + return $class::parseDateTime($value, $this->_localeFormat); + } + + /** + * Casts given value to Statement equivalent + * + * @param mixed $value value to be converted to PDO statement + * @param \Cake\Database\Driver $driver object from which database preferences and configuration will be extracted + * + * @return mixed + */ + public function toStatement($value, Driver $driver) + { + return PDO::PARAM_STR; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/DateType.php b/app/vendor/cakephp/cakephp/src/Database/Type/DateType.php new file mode 100644 index 000000000..cf7f68d90 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/DateType.php @@ -0,0 +1,101 @@ +_setClassName('Cake\I18n\FrozenDate', 'DateTimeImmutable'); + + return $this; + } + + /** + * Change the preferred class name to the mutable Date implementation. + * + * @return $this + */ + public function useMutable() + { + $this->_setClassName('Cake\I18n\Date', 'DateTime'); + + return $this; + } + + /** + * Convert request data into a datetime object. + * + * @param mixed $value Request data + * @return \DateTimeInterface + */ + public function marshal($value) + { + $date = parent::marshal($value); + if ($date instanceof DateTime) { + $date->setTime(0, 0, 0); + } + + return $date; + } + + /** + * {@inheritDoc} + */ + protected function _parseValue($value) + { + /* @var \Cake\I18n\Time $class */ + $class = $this->_className; + + return $class::parseDate($value, $this->_localeFormat); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/DecimalType.php b/app/vendor/cakephp/cakephp/src/Database/Type/DecimalType.php new file mode 100644 index 000000000..c4e48d1b9 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/DecimalType.php @@ -0,0 +1,207 @@ +_name = $name; + } + + /** + * The class to use for representing number objects + * + * @var string + */ + public static $numberClass = 'Cake\I18n\Number'; + + /** + * Whether numbers should be parsed using a locale aware parser + * when marshalling string inputs. + * + * @var bool + */ + protected $_useLocaleParser = false; + + /** + * Convert integer data into the database format. + * + * @param string|int|float $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return string|null + * @throws \InvalidArgumentException + */ + public function toDatabase($value, Driver $driver) + { + if ($value === null || $value === '') { + return null; + } + if (!is_scalar($value)) { + throw new InvalidArgumentException(sprintf( + 'Cannot convert value of type `%s` to a decimal', + getTypeName($value) + )); + } + if (is_string($value) && is_numeric($value)) { + return $value; + } + + return sprintf('%F', $value); + } + + /** + * Convert float values to PHP floats + * + * @param null|string|resource $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return float|null + * @throws \Cake\Core\Exception\Exception + */ + public function toPHP($value, Driver $driver) + { + if ($value === null) { + return $value; + } + + return (float)$value; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function manyToPHP(array $values, array $fields, Driver $driver) + { + foreach ($fields as $field) { + if (!isset($values[$field])) { + continue; + } + + $values[$field] = (float)$values[$field]; + } + + return $values; + } + + /** + * Get the correct PDO binding type for integer data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\Driver $driver The driver. + * @return int + */ + public function toStatement($value, Driver $driver) + { + return PDO::PARAM_STR; + } + + /** + * Marshalls request data into PHP floats. + * + * @param mixed $value The value to convert. + * @return mixed Converted value. + */ + public function marshal($value) + { + if ($value === null || $value === '') { + return null; + } + if (is_string($value) && $this->_useLocaleParser) { + return $this->_parseValue($value); + } + if (is_numeric($value)) { + return (float)$value; + } + if (is_array($value)) { + return 1; + } + + return $value; + } + + /** + * Sets whether or not to parse numbers passed to the marshal() function + * by using a locale aware parser. + * + * @param bool $enable Whether or not to enable + * @return $this + */ + public function useLocaleParser($enable = true) + { + if ($enable === false) { + $this->_useLocaleParser = $enable; + + return $this; + } + if (static::$numberClass === 'Cake\I18n\Number' || + is_subclass_of(static::$numberClass, 'Cake\I18n\Number') + ) { + $this->_useLocaleParser = $enable; + + return $this; + } + throw new RuntimeException( + sprintf('Cannot use locale parsing with the %s class', static::$numberClass) + ); + } + + /** + * Converts a string into a float point after parsing it using the locale + * aware parser. + * + * @param string $value The value to parse and convert to an float. + * @return float + */ + protected function _parseValue($value) + { + /* @var \Cake\I18n\Number $class */ + $class = static::$numberClass; + + return $class::parseFloat($value); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeCasterTrait.php b/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeCasterTrait.php new file mode 100644 index 000000000..51c624757 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeCasterTrait.php @@ -0,0 +1,79 @@ +toExpression($value); + } + + /** + * Returns an array with the types that require values to + * be casted to expressions, out of the list of type names + * passed as parameter. + * + * @param array $types List of type names + * @return array + */ + protected function _requiresToExpressionCasting($types) + { + $result = []; + $types = array_filter($types); + foreach ($types as $k => $type) { + $object = Type::build($type); + if ($object instanceof ExpressionTypeInterface) { + $result[$k] = $object; + } + } + + return $result; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeInterface.php b/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeInterface.php new file mode 100644 index 000000000..d2f931e73 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeInterface.php @@ -0,0 +1,33 @@ +_name = $name; + } + + /** + * The class to use for representing number objects + * + * @var string + */ + public static $numberClass = 'Cake\I18n\Number'; + + /** + * Whether numbers should be parsed using a locale aware parser + * when marshalling string inputs. + * + * @var bool + */ + protected $_useLocaleParser = false; + + /** + * Convert integer data into the database format. + * + * @param string|resource $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return float|null + */ + public function toDatabase($value, Driver $driver) + { + if ($value === null || $value === '') { + return null; + } + + return (float)$value; + } + + /** + * Convert float values to PHP integers + * + * @param null|string|resource $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return float|null + * @throws \Cake\Core\Exception\Exception + */ + public function toPHP($value, Driver $driver) + { + if ($value === null) { + return null; + } + + return (float)$value; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function manyToPHP(array $values, array $fields, Driver $driver) + { + foreach ($fields as $field) { + if (!isset($values[$field])) { + continue; + } + + $values[$field] = (float)$values[$field]; + } + + return $values; + } + + /** + * Get the correct PDO binding type for integer data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\Driver $driver The driver. + * @return int + */ + public function toStatement($value, Driver $driver) + { + return PDO::PARAM_STR; + } + + /** + * Marshalls request data into PHP floats. + * + * @param mixed $value The value to convert. + * @return float|null Converted value. + */ + public function marshal($value) + { + if ($value === null || $value === '') { + return null; + } + if (is_numeric($value)) { + return (float)$value; + } + if (is_string($value) && $this->_useLocaleParser) { + return $this->_parseValue($value); + } + if (is_array($value)) { + return 1.0; + } + + return $value; + } + + /** + * Sets whether or not to parse numbers passed to the marshal() function + * by using a locale aware parser. + * + * @param bool $enable Whether or not to enable + * @return $this + */ + public function useLocaleParser($enable = true) + { + if ($enable === false) { + $this->_useLocaleParser = $enable; + + return $this; + } + if (static::$numberClass === 'Cake\I18n\Number' || + is_subclass_of(static::$numberClass, 'Cake\I18n\Number') + ) { + $this->_useLocaleParser = $enable; + + return $this; + } + throw new RuntimeException( + sprintf('Cannot use locale parsing with the %s class', static::$numberClass) + ); + } + + /** + * Converts a string into a float point after parsing it using the locale + * aware parser. + * + * @param string $value The value to parse and convert to an float. + * @return float + */ + protected function _parseValue($value) + { + $class = static::$numberClass; + + return $class::parseFloat($value); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/IntegerType.php b/app/vendor/cakephp/cakephp/src/Database/Type/IntegerType.php new file mode 100644 index 000000000..b8903ea5b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/IntegerType.php @@ -0,0 +1,142 @@ +_name = $name; + } + + /** + * Convert integer data into the database format. + * + * @param mixed $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return int|null + */ + public function toDatabase($value, Driver $driver) + { + if ($value === null || $value === '') { + return null; + } + + if (!is_scalar($value)) { + throw new InvalidArgumentException(sprintf( + 'Cannot convert value of type `%s` to integer', + getTypeName($value) + )); + } + + return (int)$value; + } + + /** + * Convert integer values to PHP integers + * + * @param mixed $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return int|null + */ + public function toPHP($value, Driver $driver) + { + if ($value === null) { + return $value; + } + + return (int)$value; + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function manyToPHP(array $values, array $fields, Driver $driver) + { + foreach ($fields as $field) { + if (!isset($values[$field])) { + continue; + } + $values[$field] = (int)$values[$field]; + } + + return $values; + } + + /** + * Get the correct PDO binding type for integer data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\Driver $driver The driver. + * @return int + */ + public function toStatement($value, Driver $driver) + { + return PDO::PARAM_INT; + } + + /** + * Marshalls request data into PHP floats. + * + * @param mixed $value The value to convert. + * @return int|null Converted value. + */ + public function marshal($value) + { + if ($value === null || $value === '') { + return null; + } + if (is_numeric($value) || ctype_digit($value)) { + return (int)$value; + } + if (is_array($value)) { + return 1; + } + + return null; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/JsonType.php b/app/vendor/cakephp/cakephp/src/Database/Type/JsonType.php new file mode 100644 index 000000000..2c48dbb16 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/JsonType.php @@ -0,0 +1,122 @@ +_name = $name; + } + + /** + * Convert a value data into a JSON string + * + * @param mixed $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return string|null + */ + public function toDatabase($value, Driver $driver) + { + if (is_resource($value)) { + throw new InvalidArgumentException('Cannot convert a resource value to JSON'); + } + + return json_encode($value); + } + + /** + * Convert string values to PHP arrays. + * + * @param mixed $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return string|null|array + */ + public function toPHP($value, Driver $driver) + { + return json_decode($value, true); + } + + /** + * {@inheritDoc} + * + * @return array + */ + public function manyToPHP(array $values, array $fields, Driver $driver) + { + foreach ($fields as $field) { + if (!isset($values[$field])) { + continue; + } + + $values[$field] = json_decode($values[$field], true); + } + + return $values; + } + + /** + * Get the correct PDO binding type for string data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\Driver $driver The driver. + * @return int + */ + public function toStatement($value, Driver $driver) + { + return PDO::PARAM_STR; + } + + /** + * Marshalls request data into a JSON compatible structure. + * + * @param mixed $value The value to convert. + * @return mixed Converted value. + */ + public function marshal($value) + { + return $value; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/OptionalConvertInterface.php b/app/vendor/cakephp/cakephp/src/Database/Type/OptionalConvertInterface.php new file mode 100644 index 000000000..8d6c03a78 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/OptionalConvertInterface.php @@ -0,0 +1,31 @@ +__toString(); + } + + if (is_scalar($value)) { + return (string)$value; + } + + throw new InvalidArgumentException(sprintf( + 'Cannot convert value of type `%s` to string', + getTypeName($value) + )); + } + + /** + * Convert string values to PHP strings. + * + * @param mixed $value The value to convert. + * @param \Cake\Database\Driver $driver The driver instance to convert with. + * @return string|null + */ + public function toPHP($value, Driver $driver) + { + if ($value === null) { + return null; + } + + return (string)$value; + } + + /** + * Get the correct PDO binding type for string data. + * + * @param mixed $value The value being bound. + * @param \Cake\Database\Driver $driver The driver. + * @return int + */ + public function toStatement($value, Driver $driver) + { + return PDO::PARAM_STR; + } + + /** + * Marshalls request data into PHP strings. + * + * @param mixed $value The value to convert. + * @return string|null Converted value. + */ + public function marshal($value) + { + if ($value === null) { + return null; + } + if (is_array($value)) { + return ''; + } + + return (string)$value; + } + + /** + * {@inheritDoc} + * + * @return boolean False as database results are returned already as strings + */ + public function requiresToPhpCast() + { + return false; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/TimeType.php b/app/vendor/cakephp/cakephp/src/Database/Type/TimeType.php new file mode 100644 index 000000000..582a4e287 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/TimeType.php @@ -0,0 +1,42 @@ +_className; + + return $class::parseTime($value, $this->_localeFormat); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/UuidType.php b/app/vendor/cakephp/cakephp/src/Database/Type/UuidType.php new file mode 100644 index 000000000..ecfdb8593 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/Type/UuidType.php @@ -0,0 +1,66 @@ +toDatabase($value, $this->_driver); + $type = $type->toStatement($value, $this->_driver); + } + + return [$value, $type]; + } + + /** + * Matches columns to corresponding types + * + * Both $columns and $types should either be numeric based or string key based at + * the same time. + * + * @param array $columns list or associative array of columns and parameters to be bound with types + * @param array $types list or associative array of types + * @return array + */ + public function matchTypes($columns, $types) + { + if (!is_int(key($types))) { + $positions = array_intersect_key(array_flip($columns), $types); + $types = array_intersect_key($types, $positions); + $types = array_combine($positions, $types); + } + + return $types; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/TypeInterface.php b/app/vendor/cakephp/cakephp/src/Database/TypeInterface.php new file mode 100644 index 000000000..82e17f477 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/TypeInterface.php @@ -0,0 +1,90 @@ +setDefaults($defaults); + } + + /** + * Configures a map of default fields and their associated types to be + * used as the default list of types for every function in this class + * with a $types param. Useful to avoid repetition when calling the same + * functions using the same fields and types. + * + * ### Example + * + * ``` + * $query->setDefaults(['created' => 'datetime', 'is_visible' => 'boolean']); + * ``` + * + * This method will replace all the existing type maps with the ones provided. + * + * @param array $defaults Associative array where keys are field names and values + * are the correspondent type. + * @return $this + */ + public function setDefaults(array $defaults) + { + $this->_defaults = $defaults; + + return $this; + } + + /** + * Returns the currently configured types. + * + * @return array + */ + public function getDefaults() + { + return $this->_defaults; + } + + /** + * Configures a map of default fields and their associated types to be + * used as the default list of types for every function in this class + * with a $types param. Useful to avoid repetition when calling the same + * functions using the same fields and types. + * + * If called with no arguments it will return the currently configured types. + * + * ### Example + * + * ``` + * $query->defaults(['created' => 'datetime', 'is_visible' => 'boolean']); + * ``` + * + * This method will replace all the existing type maps with the ones provided. + * + * @deprecated 3.4.0 Use setDefaults()/getDefaults() instead. + * @param array|null $defaults associative array where keys are field names and values + * are the correspondent type. + * @return $this|array + */ + public function defaults(array $defaults = null) + { + deprecationWarning( + 'TypeMap::defaults() is deprecated. ' . + 'Use TypeMap::setDefaults()/getDefaults() instead.' + ); + if ($defaults !== null) { + return $this->setDefaults($defaults); + } + + return $this->getDefaults(); + } + + /** + * Add additional default types into the type map. + * + * If a key already exists it will not be overwritten. + * + * @param array $types The additional types to add. + * @return void + */ + public function addDefaults(array $types) + { + $this->_defaults += $types; + } + + /** + * Sets a map of fields and their associated types for single-use. + * + * ### Example + * + * ``` + * $query->setTypes(['created' => 'time']); + * ``` + * + * This method will replace all the existing type maps with the ones provided. + * + * @param array $types Associative array where keys are field names and values + * are the correspondent type. + * @return $this + */ + public function setTypes(array $types) + { + $this->_types = $types; + + return $this; + } + + /** + * Gets a map of fields and their associated types for single-use. + * + * @return array + */ + public function getTypes() + { + return $this->_types; + } + + /** + * Sets a map of fields and their associated types for single-use. + * + * If called with no arguments it will return the currently configured types. + * + * ### Example + * + * ``` + * $query->types(['created' => 'time']); + * ``` + * + * This method will replace all the existing type maps with the ones provided. + * + * @deprecated 3.4.0 Use setTypes()/getTypes() instead. + * @param array|null $types associative array where keys are field names and values + * are the correspondent type. + * @return $this|array + */ + public function types(array $types = null) + { + deprecationWarning( + 'TypeMap::types() is deprecated. ' . + 'Use TypeMap::setTypes()/getTypes() instead.' + ); + if ($types !== null) { + return $this->setTypes($types); + } + + return $this->getTypes(); + } + + /** + * Returns the type of the given column. If there is no single use type is configured, + * the column type will be looked for inside the default mapping. If neither exist, + * null will be returned. + * + * @param string $column The type for a given column + * @return null|string + */ + public function type($column) + { + if (isset($this->_types[$column])) { + return $this->_types[$column]; + } + if (isset($this->_defaults[$column])) { + return $this->_defaults[$column]; + } + + return null; + } + + /** + * Returns an array of all types mapped types + * + * @return array + */ + public function toArray() + { + return $this->_types + $this->_defaults; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/TypeMapTrait.php b/app/vendor/cakephp/cakephp/src/Database/TypeMapTrait.php new file mode 100644 index 000000000..cc2a28f9e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/TypeMapTrait.php @@ -0,0 +1,121 @@ +_typeMap = is_array($typeMap) ? new TypeMap($typeMap) : $typeMap; + + return $this; + } + + /** + * Returns the existing type map. + * + * @return \Cake\Database\TypeMap + */ + public function getTypeMap() + { + if ($this->_typeMap === null) { + $this->_typeMap = new TypeMap(); + } + + return $this->_typeMap; + } + + /** + * Creates a new TypeMap if $typeMap is an array, otherwise returns the existing type map + * or exchanges it for the given one. + * + * @deprecated 3.4.0 Use setTypeMap()/getTypeMap() instead. + * @param array|\Cake\Database\TypeMap|null $typeMap Creates a TypeMap if array, otherwise sets the given TypeMap + * @return $this|\Cake\Database\TypeMap + */ + public function typeMap($typeMap = null) + { + deprecationWarning( + 'TypeMapTrait::typeMap() is deprecated. ' . + 'Use TypeMapTrait::setTypeMap()/getTypeMap() instead.' + ); + if ($typeMap !== null) { + return $this->setTypeMap($typeMap); + } + + return $this->getTypeMap(); + } + + /** + * Allows setting default types when chaining query. + * + * @param array $types The array of types to set. + * @return $this + */ + public function setDefaultTypes(array $types) + { + $this->getTypeMap()->setDefaults($types); + + return $this; + } + + /** + * Gets default types of current type map. + * + * @return array + */ + public function getDefaultTypes() + { + return $this->getTypeMap()->getDefaults(); + } + + /** + * Allows setting default types when chaining query + * + * @deprecated 3.4.0 Use setDefaultTypes()/getDefaultTypes() instead. + * @param array|null $types The array of types to set. + * @return $this|array + */ + public function defaultTypes(array $types = null) + { + deprecationWarning( + 'TypeMapTrait::defaultTypes() is deprecated. ' . + 'Use TypeMapTrait::setDefaultTypes()/getDefaultTypes() instead.' + ); + if ($types !== null) { + return $this->setDefaultTypes($types); + } + + return $this->getDefaultTypes(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/TypedResultInterface.php b/app/vendor/cakephp/cakephp/src/Database/TypedResultInterface.php new file mode 100644 index 000000000..b77d46385 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/TypedResultInterface.php @@ -0,0 +1,35 @@ +_returnType; + } + + /** + * Sets the type of the value this object will generate. + * + * @param string $type The name of the type that is to be returned + * @return $this + */ + public function setReturnType($type) + { + $this->_returnType = $type; + + return $this; + } + + /** + * Sets the type of the value this object will generate. + * If called without arguments, returns the current known type + * + * @deprecated 3.5.0 Use getReturnType()/setReturnType() instead. + * @param string|null $type The name of the type that is to be returned + * @return string|$this + */ + public function returnType($type = null) + { + deprecationWarning( + 'TypedResultTrait::returnType() is deprecated. ' . + 'Use TypedResultTrait::setReturnType()/getReturnType() instead.' + ); + if ($type !== null) { + $this->_returnType = $type; + + return $this; + } + + return $this->_returnType; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/ValueBinder.php b/app/vendor/cakephp/cakephp/src/Database/ValueBinder.php new file mode 100644 index 000000000..a13838d46 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/ValueBinder.php @@ -0,0 +1,150 @@ +_bindings[$param] = compact('value', 'type') + [ + 'placeholder' => is_int($param) ? $param : substr($param, 1) + ]; + } + + /** + * Creates a unique placeholder name if the token provided does not start with ":" + * otherwise, it will return the same string and internally increment the number + * of placeholders generated by this object. + * + * @param string $token string from which the placeholder will be derived from, + * if it starts with a colon, then the same string is returned + * @return string to be used as a placeholder in a query expression + */ + public function placeholder($token) + { + $number = $this->_bindingsCount++; + if ($token[0] !== ':' && $token !== '?') { + $token = sprintf(':%s%s', $token, $number); + } + + return $token; + } + + /** + * Creates unique named placeholders for each of the passed values + * and binds them with the specified type. + * + * @param array|\Traversable $values The list of values to be bound + * @param string $type The type with which all values will be bound + * @return array with the placeholders to insert in the query + */ + public function generateManyNamed($values, $type = 'string') + { + $placeholders = []; + foreach ($values as $k => $value) { + $param = $this->placeholder('c'); + $this->_bindings[$param] = [ + 'value' => $value, + 'type' => $type, + 'placeholder' => substr($param, 1), + ]; + $placeholders[$k] = $param; + } + + return $placeholders; + } + + /** + * Returns all values bound to this expression object at this nesting level. + * Subexpression bound values will not be returned with this function. + * + * @return array + */ + public function bindings() + { + return $this->_bindings; + } + + /** + * Clears any bindings that were previously registered + * + * @return void + */ + public function reset() + { + $this->_bindings = []; + $this->_bindingsCount = 0; + } + + /** + * Resets the bindings count without clearing previously bound values + * + * @return void + */ + public function resetCount() + { + $this->_bindingsCount = 0; + } + + /** + * Binds all the stored values in this object to the passed statement. + * + * @param \Cake\Database\StatementInterface $statement The statement to add parameters to. + * @return void + */ + public function attachTo($statement) + { + $bindings = $this->bindings(); + if (empty($bindings)) { + return; + } + + foreach ($bindings as $b) { + $statement->bindValue($b['placeholder'], $b['value'], $b['type']); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Database/composer.json b/app/vendor/cakephp/cakephp/src/Database/composer.json new file mode 100644 index 000000000..3de651356 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Database/composer.json @@ -0,0 +1,40 @@ +{ + "name": "cakephp/database", + "description": "Flexible and powerful Database abstraction library with a familiar PDO-like API", + "type": "library", + "keywords": [ + "cakephp", + "database", + "abstraction", + "database abstraction", + "pdo" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/database/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/database" + }, + "require": { + "php": ">=5.6.0", + "cakephp/cache": "^3.6.0", + "cakephp/core": "^3.6.0", + "cakephp/datasource": "^3.6.0" + }, + "suggest": { + "cakephp/log": "Require this if you want to use the built-in query logger" + }, + "autoload": { + "psr-4": { + "Cake\\Database\\": "." + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/ConnectionInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/ConnectionInterface.php new file mode 100644 index 000000000..fbc3b2417 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/ConnectionInterface.php @@ -0,0 +1,92 @@ + 'Cake\Database\Driver\Mysql', + 'postgres' => 'Cake\Database\Driver\Postgres', + 'sqlite' => 'Cake\Database\Driver\Sqlite', + 'sqlserver' => 'Cake\Database\Driver\Sqlserver', + ]; + + /** + * The ConnectionRegistry used by the manager. + * + * @var \Cake\Datasource\ConnectionRegistry + */ + protected static $_registry; + + /** + * Configure a new connection object. + * + * The connection will not be constructed until it is first used. + * + * @param string|array $key The name of the connection config, or an array of multiple configs. + * @param array|null $config An array of name => config data for adapter. + * @return void + * @throws \Cake\Core\Exception\Exception When trying to modify an existing config. + * @see \Cake\Core\StaticConfigTrait::config() + */ + public static function setConfig($key, $config = null) + { + if (is_array($config)) { + $config['name'] = $key; + } + + static::_setConfig($key, $config); + } + + /** + * Parses a DSN into a valid connection configuration + * + * This method allows setting a DSN using formatting similar to that used by PEAR::DB. + * The following is an example of its usage: + * + * ``` + * $dsn = 'mysql://user:pass@localhost/database'; + * $config = ConnectionManager::parseDsn($dsn); + * + * $dsn = 'Cake\Database\Driver\Mysql://localhost:3306/database?className=Cake\Database\Connection'; + * $config = ConnectionManager::parseDsn($dsn); + * + * $dsn = 'Cake\Database\Connection://localhost:3306/database?driver=Cake\Database\Driver\Mysql'; + * $config = ConnectionManager::parseDsn($dsn); + * ``` + * + * For all classes, the value of `scheme` is set as the value of both the `className` and `driver` + * unless they have been otherwise specified. + * + * Note that query-string arguments are also parsed and set as values in the returned configuration. + * + * @param string|null $config The DSN string to convert to a configuration array + * @return array The configuration array to be stored after parsing the DSN + */ + public static function parseDsn($config = null) + { + $config = static::_parseDsn($config); + + if (isset($config['path']) && empty($config['database'])) { + $config['database'] = substr($config['path'], 1); + } + + if (empty($config['driver'])) { + $config['driver'] = $config['className']; + $config['className'] = 'Cake\Database\Connection'; + } + + unset($config['path']); + + return $config; + } + + /** + * Set one or more connection aliases. + * + * Connection aliases allow you to rename active connections without overwriting + * the aliased connection. This is most useful in the test-suite for replacing + * connections with their test variant. + * + * Defined aliases will take precedence over normal connection names. For example, + * if you alias 'default' to 'test', fetching 'default' will always return the 'test' + * connection as long as the alias is defined. + * + * You can remove aliases with ConnectionManager::dropAlias(). + * + * ### Usage + * + * ``` + * // Make 'things' resolve to 'test_things' connection + * ConnectionManager::alias('test_things', 'things'); + * ``` + * + * @param string $alias The alias to add. Fetching $source will return $alias when loaded with get. + * @param string $source The connection to add an alias to. + * @return void + * @throws \Cake\Datasource\Exception\MissingDatasourceConfigException When aliasing a + * connection that does not exist. + */ + public static function alias($alias, $source) + { + if (empty(static::$_config[$source]) && empty(static::$_config[$alias])) { + throw new MissingDatasourceConfigException( + sprintf('Cannot create alias of "%s" as it does not exist.', $alias) + ); + } + static::$_aliasMap[$source] = $alias; + } + + /** + * Drop an alias. + * + * Removes an alias from ConnectionManager. Fetching the aliased + * connection may fail if there is no other connection with that name. + * + * @param string $name The connection name to remove aliases for. + * @return void + */ + public static function dropAlias($name) + { + unset(static::$_aliasMap[$name]); + } + + /** + * Get a connection. + * + * If the connection has not been constructed an instance will be added + * to the registry. This method will use any aliases that have been + * defined. If you want the original unaliased connections pass `false` + * as second parameter. + * + * @param string $name The connection name. + * @param bool $useAliases Set to false to not use aliased connections. + * @return \Cake\Datasource\ConnectionInterface A connection object. + * @throws \Cake\Datasource\Exception\MissingDatasourceConfigException When config + * data is missing. + */ + public static function get($name, $useAliases = true) + { + if ($useAliases && isset(static::$_aliasMap[$name])) { + $name = static::$_aliasMap[$name]; + } + if (empty(static::$_config[$name])) { + throw new MissingDatasourceConfigException(['name' => $name]); + } + if (empty(static::$_registry)) { + static::$_registry = new ConnectionRegistry(); + } + if (isset(static::$_registry->{$name})) { + return static::$_registry->{$name}; + } + + return static::$_registry->load($name, static::$_config[$name]); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/ConnectionRegistry.php b/app/vendor/cakephp/cakephp/src/Datasource/ConnectionRegistry.php new file mode 100644 index 000000000..ea9d434dd --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/ConnectionRegistry.php @@ -0,0 +1,102 @@ + $class, + 'plugin' => $plugin, + ]); + } + + /** + * Create the connection object with the correct settings. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * + * If a callable is passed as first argument, The returned value of this + * function will be the result of the callable. + * + * @param string|object|callable $class The classname or object to make. + * @param string $alias The alias of the object. + * @param array $settings An array of settings to use for the datasource. + * @return object A connection with the correct settings. + */ + protected function _create($class, $alias, $settings) + { + if (is_callable($class)) { + return $class($alias); + } + + if (is_object($class)) { + return $class; + } + + unset($settings['className']); + + return new $class($settings); + } + + /** + * Remove a single adapter from the registry. + * + * @param string $name The adapter name. + * @return void + */ + public function unload($name) + { + unset($this->_loaded[$name]); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/EntityInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/EntityInterface.php new file mode 100644 index 000000000..d10832a8a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/EntityInterface.php @@ -0,0 +1,207 @@ +accessible('*', true)` means that any property not specified already + * will be accessible by default. + * + * @deprecated 3.4.0 Use setAccess() and isAccessible() instead. + * @param string|array $property Either a single or list of properties to change its accessibility. + * @param bool|null $set true marks the property as accessible, false will + * mark it as protected. + * @return \Cake\Datasource\EntityInterface|bool + */ + public function accessible($property, $set = null); +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/EntityTrait.php b/app/vendor/cakephp/cakephp/src/Datasource/EntityTrait.php new file mode 100644 index 000000000..b433dd19f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/EntityTrait.php @@ -0,0 +1,1418 @@ + true` + * means that any property not defined in the map will be accessible by default + * + * @var array + */ + protected $_accessible = ['*' => true]; + + /** + * The alias of the repository this entity came from + * + * @var string + */ + protected $_registryAlias; + + /** + * Magic getter to access properties that have been set in this entity + * + * @param string $property Name of the property to access + * @return mixed + */ + public function &__get($property) + { + return $this->get($property); + } + + /** + * Magic setter to add or edit a property in this entity + * + * @param string $property The name of the property to set + * @param mixed $value The value to set to the property + * @return void + */ + public function __set($property, $value) + { + $this->set($property, $value); + } + + /** + * Returns whether this entity contains a property named $property + * regardless of if it is empty. + * + * @param string $property The property to check. + * @return bool + * @see \Cake\ORM\Entity::has() + */ + public function __isset($property) + { + return $this->has($property); + } + + /** + * Removes a property from this entity + * + * @param string $property The property to unset + * @return void + */ + public function __unset($property) + { + $this->unsetProperty($property); + } + + /** + * Sets a single property inside this entity. + * + * ### Example: + * + * ``` + * $entity->set('name', 'Andrew'); + * ``` + * + * It is also possible to mass-assign multiple properties to this entity + * with one call by passing a hashed array as properties in the form of + * property => value pairs + * + * ### Example: + * + * ``` + * $entity->set(['name' => 'andrew', 'id' => 1]); + * echo $entity->name // prints andrew + * echo $entity->id // prints 1 + * ``` + * + * Some times it is handy to bypass setter functions in this entity when assigning + * properties. You can achieve this by disabling the `setter` option using the + * `$options` parameter: + * + * ``` + * $entity->set('name', 'Andrew', ['setter' => false]); + * $entity->set(['name' => 'Andrew', 'id' => 1], ['setter' => false]); + * ``` + * + * Mass assignment should be treated carefully when accepting user input, by default + * entities will guard all fields when properties are assigned in bulk. You can disable + * the guarding for a single set call with the `guard` option: + * + * ``` + * $entity->set(['name' => 'Andrew', 'id' => 1], ['guard' => true]); + * ``` + * + * You do not need to use the guard option when assigning properties individually: + * + * ``` + * // No need to use the guard option. + * $entity->set('name', 'Andrew'); + * ``` + * + * @param string|array $property the name of property to set or a list of + * properties with their respective values + * @param mixed $value The value to set to the property or an array if the + * first argument is also an array, in which case will be treated as $options + * @param array $options options to be used for setting the property. Allowed option + * keys are `setter` and `guard` + * @return $this + * @throws \InvalidArgumentException + */ + public function set($property, $value = null, array $options = []) + { + if (is_string($property) && $property !== '') { + $guard = false; + $property = [$property => $value]; + } else { + $guard = true; + $options = (array)$value; + } + + if (!is_array($property)) { + throw new InvalidArgumentException('Cannot set an empty property'); + } + $options += ['setter' => true, 'guard' => $guard]; + + foreach ($property as $p => $value) { + if ($options['guard'] === true && !$this->isAccessible($p)) { + continue; + } + + $this->setDirty($p, true); + + if (!array_key_exists($p, $this->_original) && + array_key_exists($p, $this->_properties) && + $this->_properties[$p] !== $value + ) { + $this->_original[$p] = $this->_properties[$p]; + } + + if (!$options['setter']) { + $this->_properties[$p] = $value; + continue; + } + + $setter = static::_accessor($p, 'set'); + if ($setter) { + $value = $this->{$setter}($value); + } + $this->_properties[$p] = $value; + } + + return $this; + } + + /** + * Returns the value of a property by name + * + * @param string $property the name of the property to retrieve + * @return mixed + * @throws \InvalidArgumentException if an empty property name is passed + */ + public function &get($property) + { + if (!strlen((string)$property)) { + throw new InvalidArgumentException('Cannot get an empty property'); + } + + $value = null; + $method = static::_accessor($property, 'get'); + + if (isset($this->_properties[$property])) { + $value =& $this->_properties[$property]; + } + + if ($method) { + $result = $this->{$method}($value); + + return $result; + } + + return $value; + } + + /** + * Returns the value of an original property by name + * + * @param string $property the name of the property for which original value is retrieved. + * @return mixed + * @throws \InvalidArgumentException if an empty property name is passed. + */ + public function getOriginal($property) + { + if (!strlen((string)$property)) { + throw new InvalidArgumentException('Cannot get an empty property'); + } + if (array_key_exists($property, $this->_original)) { + return $this->_original[$property]; + } + + return $this->get($property); + } + + /** + * Gets all original values of the entity. + * + * @return array + */ + public function getOriginalValues() + { + $originals = $this->_original; + $originalKeys = array_keys($originals); + foreach ($this->_properties as $key => $value) { + if (!in_array($key, $originalKeys)) { + $originals[$key] = $value; + } + } + + return $originals; + } + + /** + * Returns whether this entity contains a property named $property + * that contains a non-null value. + * + * ### Example: + * + * ``` + * $entity = new Entity(['id' => 1, 'name' => null]); + * $entity->has('id'); // true + * $entity->has('name'); // false + * $entity->has('last_name'); // false + * ``` + * + * You can check multiple properties by passing an array: + * + * ``` + * $entity->has(['name', 'last_name']); + * ``` + * + * All properties must not be null to get a truthy result. + * + * When checking multiple properties. All properties must not be null + * in order for true to be returned. + * + * @param string|array $property The property or properties to check. + * @return bool + */ + public function has($property) + { + foreach ((array)$property as $prop) { + if ($this->get($prop) === null) { + return false; + } + } + + return true; + } + + /** + * Checks that a property is empty + * + * This is not working like the PHP `empty()` function. The method will + * return true for: + * + * - `''` (empty string) + * - `null` + * - `[]` + * + * and false in all other cases. + * + * @param string $property The property to check. + * @return bool + */ + public function isEmpty($property) + { + $value = $this->get($property); + if ($value === null + || (is_array($value) && empty($value) + || (is_string($value) && empty($value))) + ) { + return true; + } + + return false; + } + + /** + * Checks tha a property has a value. + * + * This method will return true for + * + * - Non-empty strings + * - Non-empty arrays + * - Any object + * - Integer, even `0` + * - Float, even 0.0 + * + * and false in all other cases. + * + * @param string $property The property to check. + * @return bool + */ + public function hasValue($property) + { + return !$this->isEmpty($property); + } + + /** + * Removes a property or list of properties from this entity + * + * ### Examples: + * + * ``` + * $entity->unsetProperty('name'); + * $entity->unsetProperty(['name', 'last_name']); + * ``` + * + * @param string|array $property The property to unset. + * @return $this + */ + public function unsetProperty($property) + { + $property = (array)$property; + foreach ($property as $p) { + unset($this->_properties[$p], $this->_dirty[$p]); + } + + return $this; + } + + /** + * Get/Set the hidden properties on this entity. + * + * If the properties argument is null, the currently hidden properties + * will be returned. Otherwise the hidden properties will be set. + * + * @deprecated 3.4.0 Use EntityTrait::setHidden() and EntityTrait::getHidden() + * @param null|array $properties Either an array of properties to hide or null to get properties + * @return array|$this + */ + public function hiddenProperties($properties = null) + { + deprecationWarning( + get_called_class() . '::hiddenProperties() is deprecated. ' . + 'Use setHidden()/getHidden() instead.' + ); + if ($properties === null) { + return $this->_hidden; + } + $this->_hidden = $properties; + + return $this; + } + + /** + * Sets hidden properties. + * + * @param array $properties An array of properties to hide from array exports. + * @param bool $merge Merge the new properties with the existing. By default false. + * @return $this + */ + public function setHidden(array $properties, $merge = false) + { + if ($merge === false) { + $this->_hidden = $properties; + + return $this; + } + + $properties = array_merge($this->_hidden, $properties); + $this->_hidden = array_unique($properties); + + return $this; + } + + /** + * Gets the hidden properties. + * + * @return array + */ + public function getHidden() + { + return $this->_hidden; + } + + /** + * Get/Set the virtual properties on this entity. + * + * If the properties argument is null, the currently virtual properties + * will be returned. Otherwise the virtual properties will be set. + * + * @deprecated 3.4.0 Use EntityTrait::getVirtual() and EntityTrait::setVirtual() + * @param null|array $properties Either an array of properties to treat as virtual or null to get properties + * @return array|$this + */ + public function virtualProperties($properties = null) + { + deprecationWarning( + get_called_class() . '::virtualProperties() is deprecated. ' . + 'Use setVirtual()/getVirtual() instead.' + ); + if ($properties === null) { + return $this->getVirtual(); + } + + return $this->setVirtual($properties); + } + + /** + * Sets the virtual properties on this entity. + * + * @param array $properties An array of properties to treat as virtual. + * @param bool $merge Merge the new properties with the existing. By default false. + * @return $this + */ + public function setVirtual(array $properties, $merge = false) + { + if ($merge === false) { + $this->_virtual = $properties; + + return $this; + } + + $properties = array_merge($this->_virtual, $properties); + $this->_virtual = array_unique($properties); + + return $this; + } + + /** + * Gets the virtual properties on this entity. + * + * @return array + */ + public function getVirtual() + { + return $this->_virtual; + } + + /** + * Get the list of visible properties. + * + * The list of visible properties is all standard properties + * plus virtual properties minus hidden properties. + * + * @return array A list of properties that are 'visible' in all + * representations. + */ + public function visibleProperties() + { + $properties = array_keys($this->_properties); + $properties = array_merge($properties, $this->_virtual); + + return array_diff($properties, $this->_hidden); + } + + /** + * Returns an array with all the properties that have been set + * to this entity + * + * This method will recursively transform entities assigned to properties + * into arrays as well. + * + * @return array + */ + public function toArray() + { + $result = []; + foreach ($this->visibleProperties() as $property) { + $value = $this->get($property); + if (is_array($value)) { + $result[$property] = []; + foreach ($value as $k => $entity) { + if ($entity instanceof EntityInterface) { + $result[$property][$k] = $entity->toArray(); + } else { + $result[$property][$k] = $entity; + } + } + } elseif ($value instanceof EntityInterface) { + $result[$property] = $value->toArray(); + } else { + $result[$property] = $value; + } + } + + return $result; + } + + /** + * Returns the properties that will be serialized as JSON + * + * @return array + */ + public function jsonSerialize() + { + return $this->extract($this->visibleProperties()); + } + + /** + * Implements isset($entity); + * + * @param mixed $offset The offset to check. + * @return bool Success + */ + public function offsetExists($offset) + { + return $this->has($offset); + } + + /** + * Implements $entity[$offset]; + * + * @param mixed $offset The offset to get. + * @return mixed + */ + public function &offsetGet($offset) + { + return $this->get($offset); + } + + /** + * Implements $entity[$offset] = $value; + * + * @param mixed $offset The offset to set. + * @param mixed $value The value to set. + * @return void + */ + public function offsetSet($offset, $value) + { + $this->set($offset, $value); + } + + /** + * Implements unset($result[$offset]); + * + * @param mixed $offset The offset to remove. + * @return void + */ + public function offsetUnset($offset) + { + $this->unsetProperty($offset); + } + + /** + * Fetch accessor method name + * Accessor methods (available or not) are cached in $_accessors + * + * @param string $property the field name to derive getter name from + * @param string $type the accessor type ('get' or 'set') + * @return string method name or empty string (no method available) + */ + protected static function _accessor($property, $type) + { + $class = static::class; + + if (isset(static::$_accessors[$class][$type][$property])) { + return static::$_accessors[$class][$type][$property]; + } + + if (!empty(static::$_accessors[$class])) { + return static::$_accessors[$class][$type][$property] = ''; + } + + if ($class === 'Cake\ORM\Entity') { + return ''; + } + + foreach (get_class_methods($class) as $method) { + $prefix = substr($method, 1, 3); + if ($method[0] !== '_' || ($prefix !== 'get' && $prefix !== 'set')) { + continue; + } + $field = lcfirst(substr($method, 4)); + $snakeField = Inflector::underscore($field); + $titleField = ucfirst($field); + static::$_accessors[$class][$prefix][$snakeField] = $method; + static::$_accessors[$class][$prefix][$field] = $method; + static::$_accessors[$class][$prefix][$titleField] = $method; + } + + if (!isset(static::$_accessors[$class][$type][$property])) { + static::$_accessors[$class][$type][$property] = ''; + } + + return static::$_accessors[$class][$type][$property]; + } + + /** + * Returns an array with the requested properties + * stored in this entity, indexed by property name + * + * @param array $properties list of properties to be returned + * @param bool $onlyDirty Return the requested property only if it is dirty + * @return array + */ + public function extract(array $properties, $onlyDirty = false) + { + $result = []; + foreach ($properties as $property) { + if (!$onlyDirty || $this->isDirty($property)) { + $result[$property] = $this->get($property); + } + } + + return $result; + } + + /** + * Returns an array with the requested original properties + * stored in this entity, indexed by property name. + * + * Properties that are unchanged from their original value will be included in the + * return of this method. + * + * @param array $properties List of properties to be returned + * @return array + */ + public function extractOriginal(array $properties) + { + $result = []; + foreach ($properties as $property) { + $result[$property] = $this->getOriginal($property); + } + + return $result; + } + + /** + * Returns an array with only the original properties + * stored in this entity, indexed by property name. + * + * This method will only return properties that have been modified since + * the entity was built. Unchanged properties will be omitted. + * + * @param array $properties List of properties to be returned + * @return array + */ + public function extractOriginalChanged(array $properties) + { + $result = []; + foreach ($properties as $property) { + $original = $this->getOriginal($property); + if ($original !== $this->get($property)) { + $result[$property] = $original; + } + } + + return $result; + } + + /** + * Sets the dirty status of a single property. If called with no second + * argument, it will return whether the property was modified or not + * after the object creation. + * + * When called with no arguments it will return whether or not there are any + * dirty property in the entity + * + * @deprecated 3.4.0 Use EntityTrait::setDirty() and EntityTrait::isDirty() + * @param string|null $property the field to set or check status for + * @param null|bool $isDirty true means the property was changed, false means + * it was not changed and null will make the function return current state + * for that property + * @return bool Whether the property was changed or not + */ + public function dirty($property = null, $isDirty = null) + { + deprecationWarning( + get_called_class() . '::dirty() is deprecated. ' . + 'Use setDirty()/isDirty() instead.' + ); + if ($property === null) { + return $this->isDirty(); + } + + if ($isDirty === null) { + return $this->isDirty($property); + } + + $this->setDirty($property, $isDirty); + + return true; + } + + /** + * Sets the dirty status of a single property. + * + * @param string $property the field to set or check status for + * @param bool $isDirty true means the property was changed, false means + * it was not changed. Defaults to true. + * @return $this + */ + public function setDirty($property, $isDirty = true) + { + if ($isDirty === false) { + unset($this->_dirty[$property]); + + return $this; + } + + $this->_dirty[$property] = true; + unset($this->_errors[$property], $this->_invalid[$property]); + + return $this; + } + + /** + * Checks if the entity is dirty or if a single property of it is dirty. + * + * @param string|null $property The field to check the status for. Null for the whole entity. + * @return bool Whether the property was changed or not + */ + public function isDirty($property = null) + { + if ($property === null) { + return !empty($this->_dirty); + } + + return isset($this->_dirty[$property]); + } + + /** + * Gets the dirty properties. + * + * @return array + */ + public function getDirty() + { + return array_keys($this->_dirty); + } + + /** + * Sets the entire entity as clean, which means that it will appear as + * no properties being modified or added at all. This is an useful call + * for an initial object hydration + * + * @return void + */ + public function clean() + { + $this->_dirty = []; + $this->_errors = []; + $this->_invalid = []; + $this->_original = []; + } + + /** + * Returns whether or not this entity has already been persisted. + * This method can return null in the case there is no prior information on + * the status of this entity. + * + * If called with a boolean it will set the known status of this instance, + * true means that the instance is not yet persisted in the database, false + * that it already is. + * + * @param bool|null $new true if it is known this instance was not yet persisted + * @return bool Whether or not the entity has been persisted. + */ + public function isNew($new = null) + { + if ($new === null) { + return $this->_new; + } + + $new = (bool)$new; + + if ($new) { + foreach ($this->_properties as $k => $p) { + $this->_dirty[$k] = true; + } + } + + return $this->_new = $new; + } + + /** + * Returns all validation errors. + * + * @return array + */ + public function getErrors() + { + $diff = array_diff_key($this->_properties, $this->_errors); + + return $this->_errors + (new Collection($diff)) + ->filter(function ($value) { + return is_array($value) || $value instanceof EntityInterface; + }) + ->map(function ($value) { + return $this->_readError($value); + }) + ->filter() + ->toArray(); + } + + /** + * Returns validation errors of a field + * + * @param string $field Field name to get the errors from + * @return array + */ + public function getError($field) + { + $errors = isset($this->_errors[$field]) ? $this->_errors[$field] : []; + if ($errors) { + return $errors; + } + + return $this->_nestedErrors($field); + } + + /** + * Sets error messages to the entity + * + * ## Example + * + * ``` + * // Sets the error messages for multiple fields at once + * $entity->setErrors(['salary' => ['message'], 'name' => ['another message']]); + * ``` + * + * @param array $fields The array of errors to set. + * @param bool $overwrite Whether or not to overwrite pre-existing errors for $fields + * @return $this + */ + public function setErrors(array $fields, $overwrite = false) + { + if ($overwrite) { + foreach ($fields as $f => $error) { + $this->_errors[$f] = (array)$error; + } + + return $this; + } + + foreach ($fields as $f => $error) { + $this->_errors += [$f => []]; + + // String messages are appended to the list, + // while more complex error structures need their + // keys perserved for nested validator. + if (is_string($error)) { + $this->_errors[$f][] = $error; + } else { + foreach ($error as $k => $v) { + $this->_errors[$f][$k] = $v; + } + } + } + + return $this; + } + + /** + * Sets errors for a single field + * + * ### Example + * + * ``` + * // Sets the error messages for a single field + * $entity->setError('salary', ['must be numeric', 'must be a positive number']); + * ``` + * + * @param string $field The field to get errors for, or the array of errors to set. + * @param string|array $errors The errors to be set for $field + * @param bool $overwrite Whether or not to overwrite pre-existing errors for $field + * @return $this + */ + public function setError($field, $errors, $overwrite = false) + { + if (is_string($errors)) { + $errors = [$errors]; + } + + return $this->setErrors([$field => $errors], $overwrite); + } + + /** + * Sets the error messages for a field or a list of fields. When called + * without the second argument it returns the validation + * errors for the specified fields. If called with no arguments it returns + * all the validation error messages stored in this entity and any other nested + * entity. + * + * ### Example + * + * ``` + * // Sets the error messages for a single field + * $entity->errors('salary', ['must be numeric', 'must be a positive number']); + * + * // Returns the error messages for a single field + * $entity->errors('salary'); + * + * // Returns all error messages indexed by field name + * $entity->errors(); + * + * // Sets the error messages for multiple fields at once + * $entity->errors(['salary' => ['message'], 'name' => ['another message']); + * ``` + * + * When used as a setter, this method will return this entity instance for method + * chaining. + * + * @deprecated 3.4.0 Use EntityTrait::setError(), EntityTrait::setErrors(), EntityTrait::getError() and EntityTrait::getErrors() + * @param string|array|null $field The field to get errors for, or the array of errors to set. + * @param string|array|null $errors The errors to be set for $field + * @param bool $overwrite Whether or not to overwrite pre-existing errors for $field + * @return array|$this + */ + public function errors($field = null, $errors = null, $overwrite = false) + { + deprecationWarning( + get_called_class() . '::errors() is deprecated. ' . + 'Use setError()/getError() or setErrors()/getErrors() instead.' + ); + if ($field === null) { + return $this->getErrors(); + } + + if (is_string($field) && $errors === null) { + return $this->getError($field); + } + + if (!is_array($field)) { + $field = [$field => $errors]; + } + + return $this->setErrors($field, $overwrite); + } + + /** + * Auxiliary method for getting errors in nested entities + * + * @param string $field the field in this entity to check for errors + * @return array errors in nested entity if any + */ + protected function _nestedErrors($field) + { + $path = explode('.', $field); + + // Only one path element, check for nested entity with error. + if (count($path) === 1) { + return $this->_readError($this->get($path[0])); + } + + $entity = $this; + $len = count($path); + while ($len) { + $part = array_shift($path); + $len = count($path); + $val = null; + if ($entity instanceof EntityInterface) { + $val = $entity->get($part); + } elseif (is_array($entity)) { + $val = isset($entity[$part]) ? $entity[$part] : false; + } + + if (is_array($val) || + $val instanceof Traversable || + $val instanceof EntityInterface + ) { + $entity = $val; + } else { + $path[] = $part; + break; + } + } + if (count($path) <= 1) { + return $this->_readError($entity, array_pop($path)); + } + + return []; + } + + /** + * Read the error(s) from one or many objects. + * + * @param array|\Cake\Datasource\EntityTrait $object The object to read errors from. + * @param string|null $path The field name for errors. + * @return array + */ + protected function _readError($object, $path = null) + { + if ($path !== null && $object instanceof EntityInterface) { + return $object->getError($path); + } + if ($object instanceof EntityInterface) { + return $object->getErrors(); + } + if (is_array($object)) { + $array = array_map(function ($val) { + if ($val instanceof EntityInterface) { + return $val->getErrors(); + } + }, $object); + + return array_filter($array); + } + + return []; + } + + /** + * Get a list of invalid fields and their data for errors upon validation/patching + * + * @return array + */ + public function getInvalid() + { + return $this->_invalid; + } + + /** + * Get a single value of an invalid field. Returns null if not set. + * + * @param string $field The name of the field. + * @return mixed + */ + public function getInvalidField($field) + { + $value = isset($this->_invalid[$field]) ? $this->_invalid[$field] : null; + + return $value; + } + + /** + * Set fields as invalid and not patchable into the entity. + * + * This is useful for batch operations when one needs to get the original value for an error message after patching. + * This value could not be patched into the entity and is simply copied into the _invalid property for debugging purposes + * or to be able to log it away. + * + * @param array $fields The values to set. + * @param bool $overwrite Whether or not to overwrite pre-existing values for $field. + * @return $this + */ + public function setInvalid(array $fields, $overwrite = false) + { + foreach ($fields as $field => $value) { + if ($overwrite === true) { + $this->_invalid[$field] = $value; + continue; + } + $this->_invalid += [$field => $value]; + } + + return $this; + } + + /** + * Sets a field as invalid and not patchable into the entity. + * + * @param string $field The value to set. + * @param mixed $value The invalid value to be set for $field. + * @return $this + */ + public function setInvalidField($field, $value) + { + $this->_invalid[$field] = $value; + + return $this; + } + + /** + * Sets a field as invalid and not patchable into the entity. + * + * This is useful for batch operations when one needs to get the original value for an error message after patching. + * This value could not be patched into the entity and is simply copied into the _invalid property for debugging purposes + * or to be able to log it away. + * + * @deprecated 3.5 Use getInvalid()/getInvalidField()/setInvalid() instead. + * @param string|array|null $field The field to get invalid value for, or the value to set. + * @param mixed|null $value The invalid value to be set for $field. + * @param bool $overwrite Whether or not to overwrite pre-existing values for $field. + * @return $this|mixed + */ + public function invalid($field = null, $value = null, $overwrite = false) + { + deprecationWarning( + get_called_class() . '::invalid() is deprecated. ' . + 'Use setInvalid()/getInvalid()/getInvalidField() instead.' + ); + if ($field === null) { + return $this->_invalid; + } + + if (is_string($field) && $value === null) { + $value = isset($this->_invalid[$field]) ? $this->_invalid[$field] : null; + + return $value; + } + + if (!is_array($field)) { + $field = [$field => $value]; + } + + foreach ($field as $f => $value) { + if ($overwrite) { + $this->_invalid[$f] = $value; + continue; + } + $this->_invalid += [$f => $value]; + } + + return $this; + } + + /** + * Stores whether or not a property value can be changed or set in this entity. + * The special property `*` can also be marked as accessible or protected, meaning + * that any other property specified before will take its value. For example + * `$entity->accessible('*', true)` means that any property not specified already + * will be accessible by default. + * + * You can also call this method with an array of properties, in which case they + * will each take the accessibility value specified in the second argument. + * + * ### Example: + * + * ``` + * $entity->accessible('id', true); // Mark id as not protected + * $entity->accessible('author_id', false); // Mark author_id as protected + * $entity->accessible(['id', 'user_id'], true); // Mark both properties as accessible + * $entity->accessible('*', false); // Mark all properties as protected + * ``` + * + * When called without the second param it will return whether or not the property + * can be set. + * + * ### Example: + * + * ``` + * $entity->accessible('id'); // Returns whether it can be set or not + * ``` + * + * @deprecated 3.4.0 Use EntityTrait::setAccess() and EntityTrait::isAccessible() + * @param string|array $property single or list of properties to change its accessibility + * @param bool|null $set true marks the property as accessible, false will + * mark it as protected. + * @return $this|bool + */ + public function accessible($property, $set = null) + { + deprecationWarning( + get_called_class() . '::accessible() is deprecated. ' . + 'Use setAccess()/isAccessible() instead.' + ); + if ($set === null) { + return $this->isAccessible($property); + } + + return $this->setAccess($property, $set); + } + + /** + * Stores whether or not a property value can be changed or set in this entity. + * The special property `*` can also be marked as accessible or protected, meaning + * that any other property specified before will take its value. For example + * `$entity->setAccess('*', true)` means that any property not specified already + * will be accessible by default. + * + * You can also call this method with an array of properties, in which case they + * will each take the accessibility value specified in the second argument. + * + * ### Example: + * + * ``` + * $entity->setAccess('id', true); // Mark id as not protected + * $entity->setAccess('author_id', false); // Mark author_id as protected + * $entity->setAccess(['id', 'user_id'], true); // Mark both properties as accessible + * $entity->setAccess('*', false); // Mark all properties as protected + * ``` + * + * @param string|array $property single or list of properties to change its accessibility + * @param bool $set true marks the property as accessible, false will + * mark it as protected. + * @return $this + */ + public function setAccess($property, $set) + { + if ($property === '*') { + $this->_accessible = array_map(function ($p) use ($set) { + return (bool)$set; + }, $this->_accessible); + $this->_accessible['*'] = (bool)$set; + + return $this; + } + + foreach ((array)$property as $prop) { + $this->_accessible[$prop] = (bool)$set; + } + + return $this; + } + + /** + * Checks if a property is accessible + * + * ### Example: + * + * ``` + * $entity->isAccessible('id'); // Returns whether it can be set or not + * ``` + * + * @param string $property Property name to check + * @return bool + */ + public function isAccessible($property) + { + $value = isset($this->_accessible[$property]) ? + $this->_accessible[$property] : + null; + + return ($value === null && !empty($this->_accessible['*'])) || $value; + } + + /** + * Returns the alias of the repository from which this entity came from. + * + * @return string + */ + public function getSource() + { + return $this->_registryAlias; + } + + /** + * Sets the source alias + * + * @param string $alias the alias of the repository + * @return $this + */ + public function setSource($alias) + { + $this->_registryAlias = $alias; + + return $this; + } + + /** + * Returns the alias of the repository from which this entity came from. + * + * If called with no arguments, it returns the alias of the repository + * this entity came from if it is known. + * + * @deprecated 3.4.0 Use EntityTrait::getSource() and EntityTrait::setSource() + * @param string|null $alias the alias of the repository + * @return string|$this + */ + public function source($alias = null) + { + deprecationWarning( + get_called_class() . '::source() is deprecated. ' . + 'Use setSource()/getSource() instead.' + ); + if ($alias === null) { + return $this->getSource(); + } + + $this->setSource($alias); + + return $this; + } + + /** + * Returns a string representation of this object in a human readable format. + * + * @return string + */ + public function __toString() + { + return json_encode($this, JSON_PRETTY_PRINT); + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + $properties = $this->_properties; + foreach ($this->_virtual as $field) { + $properties[$field] = $this->$field; + } + + return $properties + [ + '[new]' => $this->isNew(), + '[accessible]' => $this->_accessible, + '[dirty]' => $this->_dirty, + '[original]' => $this->_original, + '[virtual]' => $this->_virtual, + '[errors]' => $this->_errors, + '[invalid]' => $this->_invalid, + '[repository]' => $this->_registryAlias + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/Exception/InvalidPrimaryKeyException.php b/app/vendor/cakephp/cakephp/src/Datasource/Exception/InvalidPrimaryKeyException.php new file mode 100644 index 000000000..9a033a781 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/Exception/InvalidPrimaryKeyException.php @@ -0,0 +1,41 @@ +modelClass)) { + $this->modelClass = $name; + } + } + + /** + * Loads and constructs repository objects required by this object + * + * Typically used to load ORM Table objects as required. Can + * also be used to load other types of repository objects your application uses. + * + * If a repository provider does not return an object a MissingModelException will + * be thrown. + * + * @param string|null $modelClass Name of model class to load. Defaults to $this->modelClass + * @param string|null $modelType The type of repository to load. Defaults to the modelType() value. + * @return \Cake\Datasource\RepositoryInterface The model instance created. + * @throws \Cake\Datasource\Exception\MissingModelException If the model class cannot be found. + * @throws \InvalidArgumentException When using a type that has not been registered. + * @throws \UnexpectedValueException If no model type has been defined + */ + public function loadModel($modelClass = null, $modelType = null) + { + if ($modelClass === null) { + $modelClass = $this->modelClass; + } + if ($modelType === null) { + $modelType = $this->getModelType(); + + if ($modelType === null) { + throw new UnexpectedValueException('No model type has been defined'); + } + } + + list(, $alias) = pluginSplit($modelClass, true); + + if (isset($this->{$alias})) { + return $this->{$alias}; + } + + if (isset($this->_modelFactories[$modelType])) { + $factory = $this->_modelFactories[$modelType]; + } + if (!isset($factory)) { + $factory = FactoryLocator::get($modelType); + } + $this->{$alias} = $factory($modelClass); + if (!$this->{$alias}) { + throw new MissingModelException([$modelClass, $modelType]); + } + + return $this->{$alias}; + } + + /** + * Override a existing callable to generate repositories of a given type. + * + * @param string $type The name of the repository type the factory function is for. + * @param callable $factory The factory function used to create instances. + * @return void + */ + public function modelFactory($type, callable $factory) + { + $this->_modelFactories[$type] = $factory; + } + + /** + * Get the model type to be used by this class + * + * @return string + */ + public function getModelType() + { + return $this->_modelType; + } + + /** + * Set the model type to be used by this class + * + * @param string $modelType The model type + * + * @return $this + */ + public function setModelType($modelType) + { + $this->_modelType = $modelType; + + return $this; + } + + /** + * Set or get the model type to be used by this class + * + * @deprecated 3.5.0 Use getModelType()/setModelType() instead. + * @param string|null $modelType The model type or null to retrieve the current + * + * @return string|$this + */ + public function modelType($modelType = null) + { + deprecationWarning( + get_called_class() . '::modelType() is deprecated. ' . + 'Use setModelType()/getModelType() instead.' + ); + if ($modelType === null) { + return $this->_modelType; + } + + $this->_modelType = $modelType; + + return $this; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/Paginator.php b/app/vendor/cakephp/cakephp/src/Datasource/Paginator.php new file mode 100644 index 000000000..9d8ef5a96 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/Paginator.php @@ -0,0 +1,461 @@ + 1, + 'limit' => 20, + 'maxLimit' => 100, + 'whitelist' => ['limit', 'sort', 'page', 'direction'] + ]; + + /** + * Paging params after pagination operation is done. + * + * @var array + */ + protected $_pagingParams = []; + + /** + * Handles automatic pagination of model records. + * + * ### Configuring pagination + * + * When calling `paginate()` you can use the $settings parameter to pass in + * pagination settings. These settings are used to build the queries made + * and control other pagination settings. + * + * If your settings contain a key with the current table's alias. The data + * inside that key will be used. Otherwise the top level configuration will + * be used. + * + * ``` + * $settings = [ + * 'limit' => 20, + * 'maxLimit' => 100 + * ]; + * $results = $paginator->paginate($table, $settings); + * ``` + * + * The above settings will be used to paginate any repository. You can configure + * repository specific settings by keying the settings with the repository alias. + * + * ``` + * $settings = [ + * 'Articles' => [ + * 'limit' => 20, + * 'maxLimit' => 100 + * ], + * 'Comments' => [ ... ] + * ]; + * $results = $paginator->paginate($table, $settings); + * ``` + * + * This would allow you to have different pagination settings for + * `Articles` and `Comments` repositories. + * + * ### Controlling sort fields + * + * By default CakePHP will automatically allow sorting on any column on the + * repository object being paginated. Often times you will want to allow + * sorting on either associated columns or calculated fields. In these cases + * you will need to define a whitelist of all the columns you wish to allow + * sorting on. You can define the whitelist in the `$settings` parameter: + * + * ``` + * $settings = [ + * 'Articles' => [ + * 'finder' => 'custom', + * 'sortWhitelist' => ['title', 'author_id', 'comment_count'], + * ] + * ]; + * ``` + * + * Passing an empty array as whitelist disallows sorting altogether. + * + * ### Paginating with custom finders + * + * You can paginate with any find type defined on your table using the + * `finder` option. + * + * ``` + * $settings = [ + * 'Articles' => [ + * 'finder' => 'popular' + * ] + * ]; + * $results = $paginator->paginate($table, $settings); + * ``` + * + * Would paginate using the `find('popular')` method. + * + * You can also pass an already created instance of a query to this method: + * + * ``` + * $query = $this->Articles->find('popular')->matching('Tags', function ($q) { + * return $q->where(['name' => 'CakePHP']) + * }); + * $results = $paginator->paginate($query); + * ``` + * + * ### Scoping Request parameters + * + * By using request parameter scopes you can paginate multiple queries in + * the same controller action: + * + * ``` + * $articles = $paginator->paginate($articlesQuery, ['scope' => 'articles']); + * $tags = $paginator->paginate($tagsQuery, ['scope' => 'tags']); + * ``` + * + * Each of the above queries will use different query string parameter sets + * for pagination data. An example URL paginating both results would be: + * + * ``` + * /dashboard?articles[page]=1&tags[page]=2 + * ``` + * + * @param \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface $object The table or query to paginate. + * @param array $params Request params + * @param array $settings The settings/configuration used for pagination. + * @return \Cake\Datasource\ResultSetInterface Query results + * @throws \Cake\Datasource\Exception\PageOutOfBoundsException + */ + public function paginate($object, array $params = [], array $settings = []) + { + $query = null; + if ($object instanceof QueryInterface) { + $query = $object; + $object = $query->getRepository(); + } + + $alias = $object->getAlias(); + $defaults = $this->getDefaults($alias, $settings); + $options = $this->mergeOptions($params, $defaults); + $options = $this->validateSort($object, $options); + $options = $this->checkLimit($options); + + $options += ['page' => 1, 'scope' => null]; + $options['page'] = (int)$options['page'] < 1 ? 1 : (int)$options['page']; + list($finder, $options) = $this->_extractFinder($options); + + if (empty($query)) { + $query = $object->find($finder, $options); + } else { + $query->applyOptions($options); + } + + $cleanQuery = clone $query; + $results = $query->all(); + $numResults = count($results); + $count = $cleanQuery->count(); + + $page = $options['page']; + $limit = $options['limit']; + $pageCount = max((int)ceil($count / $limit), 1); + $requestedPage = $page; + $page = min($page, $pageCount); + + $order = (array)$options['order']; + $sortDefault = $directionDefault = false; + if (!empty($defaults['order']) && count($defaults['order']) === 1) { + $sortDefault = key($defaults['order']); + $directionDefault = current($defaults['order']); + } + + $start = 0; + if ($count >= 1) { + $start = (($page - 1) * $limit) + 1; + } + $end = $start + $limit - 1; + if ($count < $end) { + $end = $count; + } + + $paging = [ + 'finder' => $finder, + 'page' => $page, + 'current' => $numResults, + 'count' => $count, + 'perPage' => $limit, + 'start' => $start, + 'end' => $end, + 'prevPage' => $page > 1, + 'nextPage' => $count > ($page * $limit), + 'pageCount' => $pageCount, + 'sort' => $options['sort'], + 'direction' => current($order), + 'limit' => $defaults['limit'] != $limit ? $limit : null, + 'sortDefault' => $sortDefault, + 'directionDefault' => $directionDefault, + 'scope' => $options['scope'], + 'completeSort' => $order, + ]; + + $this->_pagingParams = [$alias => $paging]; + + if ($requestedPage > $page) { + throw new PageOutOfBoundsException([ + 'requestedPage' => $requestedPage, + 'pagingParams' => $this->_pagingParams + ]); + } + + return $results; + } + + /** + * Extracts the finder name and options out of the provided pagination options. + * + * @param array $options the pagination options. + * @return array An array containing in the first position the finder name + * and in the second the options to be passed to it. + */ + protected function _extractFinder($options) + { + $type = !empty($options['finder']) ? $options['finder'] : 'all'; + unset($options['finder'], $options['maxLimit']); + + if (is_array($type)) { + $options = (array)current($type) + $options; + $type = key($type); + } + + return [$type, $options]; + } + + /** + * Get paging params after pagination operation. + * + * @return array + */ + public function getPagingParams() + { + return $this->_pagingParams; + } + + /** + * Merges the various options that Paginator uses. + * Pulls settings together from the following places: + * + * - General pagination settings + * - Model specific settings. + * - Request parameters + * + * The result of this method is the aggregate of all the option sets + * combined together. You can change config value `whitelist` to modify + * which options/values can be set using request parameters. + * + * @param array $params Request params. + * @param array $settings The settings to merge with the request data. + * @return array Array of merged options. + */ + public function mergeOptions($params, $settings) + { + if (!empty($settings['scope'])) { + $scope = $settings['scope']; + $params = !empty($params[$scope]) ? (array)$params[$scope] : []; + } + $params = array_intersect_key($params, array_flip($this->getConfig('whitelist'))); + + return array_merge($settings, $params); + } + + /** + * Get the settings for a $model. If there are no settings for a specific + * repository, the general settings will be used. + * + * @param string $alias Model name to get settings for. + * @param array $settings The settings which is used for combining. + * @return array An array of pagination settings for a model, + * or the general settings. + */ + public function getDefaults($alias, $settings) + { + if (isset($settings[$alias])) { + $settings = $settings[$alias]; + } + + $defaults = $this->getConfig(); + $maxLimit = isset($settings['maxLimit']) ? $settings['maxLimit'] : $defaults['maxLimit']; + $limit = isset($settings['limit']) ? $settings['limit'] : $defaults['limit']; + + if ($limit > $maxLimit) { + $limit = $maxLimit; + } + + $settings['maxLimit'] = $maxLimit; + $settings['limit'] = $limit; + + return $settings + $defaults; + } + + /** + * Validate that the desired sorting can be performed on the $object. + * + * Only fields or virtualFields can be sorted on. The direction param will + * also be sanitized. Lastly sort + direction keys will be converted into + * the model friendly order key. + * + * You can use the whitelist parameter to control which columns/fields are + * available for sorting via URL parameters. This helps prevent users from ordering large + * result sets on un-indexed values. + * + * If you need to sort on associated columns or synthetic properties you + * will need to use a whitelist. + * + * Any columns listed in the sort whitelist will be implicitly trusted. + * You can use this to sort on synthetic columns, or columns added in custom + * find operations that may not exist in the schema. + * + * The default order options provided to paginate() will be merged with the user's + * requested sorting field/direction. + * + * @param \Cake\Datasource\RepositoryInterface $object Repository object. + * @param array $options The pagination options being used for this request. + * @return array An array of options with sort + direction removed and + * replaced with order if possible. + */ + public function validateSort(RepositoryInterface $object, array $options) + { + if (isset($options['sort'])) { + $direction = null; + if (isset($options['direction'])) { + $direction = strtolower($options['direction']); + } + if (!in_array($direction, ['asc', 'desc'])) { + $direction = 'asc'; + } + $order = (isset($options['order']) && is_array($options['order'])) ? $options['order'] : []; + $options['order'] = [$options['sort'] => $direction] + $order; + } else { + $options['sort'] = null; + } + unset($options['direction']); + + if (empty($options['order'])) { + $options['order'] = []; + } + if (!is_array($options['order'])) { + return $options; + } + + $inWhitelist = false; + if (isset($options['sortWhitelist'])) { + $field = key($options['order']); + $inWhitelist = in_array($field, $options['sortWhitelist'], true); + if (!$inWhitelist) { + $options['order'] = []; + $options['sort'] = null; + + return $options; + } + } + + if ($options['sort'] === null + && count($options['order']) === 1 + && !is_numeric(key($options['order'])) + ) { + $options['sort'] = key($options['order']); + } + + $options['order'] = $this->_prefix($object, $options['order'], $inWhitelist); + + return $options; + } + + /** + * Prefixes the field with the table alias if possible. + * + * @param \Cake\Datasource\RepositoryInterface $object Repository object. + * @param array $order Order array. + * @param bool $whitelisted Whether or not the field was whitelisted. + * @return array Final order array. + */ + protected function _prefix(RepositoryInterface $object, $order, $whitelisted = false) + { + $tableAlias = $object->getAlias(); + $tableOrder = []; + foreach ($order as $key => $value) { + if (is_numeric($key)) { + $tableOrder[] = $value; + continue; + } + $field = $key; + $alias = $tableAlias; + + if (strpos($key, '.') !== false) { + list($alias, $field) = explode('.', $key); + } + $correctAlias = ($tableAlias === $alias); + + if ($correctAlias && $whitelisted) { + // Disambiguate fields in schema. As id is quite common. + if ($object->hasField($field)) { + $field = $alias . '.' . $field; + } + $tableOrder[$field] = $value; + } elseif ($correctAlias && $object->hasField($field)) { + $tableOrder[$tableAlias . '.' . $field] = $value; + } elseif (!$correctAlias && $whitelisted) { + $tableOrder[$alias . '.' . $field] = $value; + } + } + + return $tableOrder; + } + + /** + * Check the limit parameter and ensure it's within the maxLimit bounds. + * + * @param array $options An array of options with a limit key to be checked. + * @return array An array of options for pagination. + */ + public function checkLimit(array $options) + { + $options['limit'] = (int)$options['limit']; + if (empty($options['limit']) || $options['limit'] < 1) { + $options['limit'] = 1; + } + $options['limit'] = max(min($options['limit'], $options['maxLimit']), 1); + + return $options; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/PaginatorInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/PaginatorInterface.php new file mode 100644 index 000000000..54ccf31fc --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/PaginatorInterface.php @@ -0,0 +1,38 @@ +_key = $key; + + if (!is_string($config) && !($config instanceof CacheEngine)) { + throw new RuntimeException('Cache configs must be strings or CacheEngine instances.'); + } + $this->_config = $config; + } + + /** + * Load the cached results from the cache or run the query. + * + * @param object $query The query the cache read is for. + * @return \Cake\Datasource\ResultSetInterface|null Either the cached results or null. + */ + public function fetch($query) + { + $key = $this->_resolveKey($query); + $storage = $this->_resolveCacher(); + $result = $storage->read($key); + if (empty($result)) { + return null; + } + + return $result; + } + + /** + * Store the result set into the cache. + * + * @param object $query The query the cache read is for. + * @param \Traversable $results The result set to store. + * @return bool True if the data was successfully cached, false on failure + */ + public function store($query, Traversable $results) + { + $key = $this->_resolveKey($query); + $storage = $this->_resolveCacher(); + + return $storage->write($key, $results); + } + + /** + * Get/generate the cache key. + * + * @param object $query The query to generate a key for. + * @return string + * @throws \RuntimeException + */ + protected function _resolveKey($query) + { + if (is_string($this->_key)) { + return $this->_key; + } + $func = $this->_key; + $key = $func($query); + if (!is_string($key)) { + $msg = sprintf('Cache key functions must return a string. Got %s.', var_export($key, true)); + throw new RuntimeException($msg); + } + + return $key; + } + + /** + * Get the cache engine. + * + * @return \Cake\Cache\CacheEngine + */ + protected function _resolveCacher() + { + if (is_string($this->_config)) { + return Cache::engine($this->_config); + } + + return $this->_config; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/QueryInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/QueryInterface.php new file mode 100644 index 000000000..793258e5d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/QueryInterface.php @@ -0,0 +1,382 @@ + value array representing a single aliased field + * that can be passed directly to the select() method. + * The key will contain the alias and the value the actual field name. + * + * If the field is already aliased, then it will not be changed. + * If no $alias is passed, the default table for this query will be used. + * + * @param string $field The field to alias + * @param string|null $alias the alias used to prefix the field + * @return string + */ + public function aliasField($field, $alias = null); + + /** + * Runs `aliasField()` for each field in the provided list and returns + * the result under a single array. + * + * @param array $fields The fields to alias + * @param string|null $defaultAlias The default alias + * @return string[] + */ + public function aliasFields($fields, $defaultAlias = null); + + /** + * Fetch the results for this query. + * + * Will return either the results set through setResult(), or execute this query + * and return the ResultSetDecorator object ready for streaming of results. + * + * ResultSetDecorator is a traversable object that implements the methods found + * on Cake\Collection\Collection. + * + * @return \Cake\Datasource\ResultSetInterface + */ + public function all(); + + /** + * Populates or adds parts to current query clauses using an array. + * This is handy for passing all query clauses at once. The option array accepts: + * + * - fields: Maps to the select method + * - conditions: Maps to the where method + * - limit: Maps to the limit method + * - order: Maps to the order method + * - offset: Maps to the offset method + * - group: Maps to the group method + * - having: Maps to the having method + * - contain: Maps to the contain options for eager loading + * - join: Maps to the join method + * - page: Maps to the page method + * + * ### Example: + * + * ``` + * $query->applyOptions([ + * 'fields' => ['id', 'name'], + * 'conditions' => [ + * 'created >=' => '2013-01-01' + * ], + * 'limit' => 10 + * ]); + * ``` + * + * Is equivalent to: + * + * ``` + * $query + * ->select(['id', 'name']) + * ->where(['created >=' => '2013-01-01']) + * ->limit(10) + * ``` + * + * @param array $options list of query clauses to apply new parts to. + * @return $this + */ + public function applyOptions(array $options); + + /** + * Apply custom finds to against an existing query object. + * + * Allows custom find methods to be combined and applied to each other. + * + * ``` + * $repository->find('all')->find('recent'); + * ``` + * + * The above is an example of stacking multiple finder methods onto + * a single query. + * + * @param string $finder The finder method to use. + * @param array $options The options for the finder. + * @return $this Returns a modified query. + */ + public function find($finder, array $options = []); + + /** + * Returns the first result out of executing this query, if the query has not been + * executed before, it will set the limit clause to 1 for performance reasons. + * + * ### Example: + * + * ``` + * $singleUser = $query->select(['id', 'username'])->first(); + * ``` + * + * @return mixed the first result from the ResultSet + */ + public function first(); + + /** + * Returns the total amount of results for the query. + * + * @return int + */ + public function count(); + + /** + * Sets the number of records that should be retrieved from database, + * accepts an integer or an expression object that evaluates to an integer. + * In some databases, this operation might not be supported or will require + * the query to be transformed in order to limit the result set size. + * + * ### Examples + * + * ``` + * $query->limit(10) // generates LIMIT 10 + * $query->limit($query->newExpr()->add(['1 + 1'])); // LIMIT (1 + 1) + * ``` + * + * @param int $num number of records to be returned + * @return $this + */ + public function limit($num); + + /** + * Sets the number of records that should be skipped from the original result set + * This is commonly used for paginating large results. Accepts an integer or an + * expression object that evaluates to an integer. + * + * In some databases, this operation might not be supported or will require + * the query to be transformed in order to limit the result set size. + * + * ### Examples + * + * ``` + * $query->offset(10) // generates OFFSET 10 + * $query->offset($query->newExpr()->add(['1 + 1'])); // OFFSET (1 + 1) + * ``` + * + * @param int $num number of records to be skipped + * @return $this + */ + public function offset($num); + + /** + * Adds a single or multiple fields to be used in the ORDER clause for this query. + * Fields can be passed as an array of strings, array of expression + * objects, a single expression or a single string. + * + * If an array is passed, keys will be used as the field itself and the value will + * represent the order in which such field should be ordered. When called multiple + * times with the same fields as key, the last order definition will prevail over + * the others. + * + * By default this function will append any passed argument to the list of fields + * to be selected, unless the second argument is set to true. + * + * ### Examples: + * + * ``` + * $query->order(['title' => 'DESC', 'author_id' => 'ASC']); + * ``` + * + * Produces: + * + * `ORDER BY title DESC, author_id ASC` + * + * ``` + * $query->order(['title' => 'DESC NULLS FIRST'])->order('author_id'); + * ``` + * + * Will generate: + * + * `ORDER BY title DESC NULLS FIRST, author_id` + * + * ``` + * $expression = $query->newExpr()->add(['id % 2 = 0']); + * $query->order($expression)->order(['title' => 'ASC']); + * ``` + * + * Will become: + * + * `ORDER BY (id %2 = 0), title ASC` + * + * If you need to set complex expressions as order conditions, you + * should use `orderAsc()` or `orderDesc()`. + * + * @param array|string $fields fields to be added to the list + * @param bool $overwrite whether to reset order with field list or not + * @return $this + */ + public function order($fields, $overwrite = false); + + /** + * Set the page of results you want. + * + * This method provides an easier to use interface to set the limit + offset + * in the record set you want as results. If empty the limit will default to + * the existing limit clause, and if that too is empty, then `25` will be used. + * + * Pages must start at 1. + * + * @param int $num The page number you want. + * @param int|null $limit The number of rows you want in the page. If null + * the current limit clause will be used. + * @return $this + * @throws \InvalidArgumentException If page number < 1. + */ + public function page($num, $limit = null); + + /** + * Returns an array representation of the results after executing the query. + * + * @return array + */ + public function toArray(); + + /** + * Returns the default repository object that will be used by this query, + * that is, the repository that will appear in the from clause. + * + * @param \Cake\Datasource\RepositoryInterface|null $repository The default repository object to use + * @return \Cake\Datasource\RepositoryInterface|$this + */ + public function repository(RepositoryInterface $repository = null); + + /** + * Adds a condition or set of conditions to be used in the WHERE clause for this + * query. Conditions can be expressed as an array of fields as keys with + * comparison operators in it, the values for the array will be used for comparing + * the field to such literal. Finally, conditions can be expressed as a single + * string or an array of strings. + * + * When using arrays, each entry will be joined to the rest of the conditions using + * an AND operator. Consecutive calls to this function will also join the new + * conditions specified using the AND operator. Additionally, values can be + * expressed using expression objects which can include other query objects. + * + * Any conditions created with this methods can be used with any SELECT, UPDATE + * and DELETE type of queries. + * + * ### Conditions using operators: + * + * ``` + * $query->where([ + * 'posted >=' => new DateTime('3 days ago'), + * 'title LIKE' => 'Hello W%', + * 'author_id' => 1, + * ], ['posted' => 'datetime']); + * ``` + * + * The previous example produces: + * + * `WHERE posted >= 2012-01-27 AND title LIKE 'Hello W%' AND author_id = 1` + * + * Second parameter is used to specify what type is expected for each passed + * key. Valid types can be used from the mapped with Database\Type class. + * + * ### Nesting conditions with conjunctions: + * + * ``` + * $query->where([ + * 'author_id !=' => 1, + * 'OR' => ['published' => true, 'posted <' => new DateTime('now')], + * 'NOT' => ['title' => 'Hello'] + * ], ['published' => boolean, 'posted' => 'datetime'] + * ``` + * + * The previous example produces: + * + * `WHERE author_id = 1 AND (published = 1 OR posted < '2012-02-01') AND NOT (title = 'Hello')` + * + * You can nest conditions using conjunctions as much as you like. Sometimes, you + * may want to define 2 different options for the same key, in that case, you can + * wrap each condition inside a new array: + * + * `$query->where(['OR' => [['published' => false], ['published' => true]])` + * + * Keep in mind that every time you call where() with the third param set to false + * (default), it will join the passed conditions to the previous stored list using + * the AND operator. Also, using the same array key twice in consecutive calls to + * this method will not override the previous value. + * + * ### Using expressions objects: + * + * ``` + * $exp = $query->newExpr()->add(['id !=' => 100, 'author_id' != 1])->tieWith('OR'); + * $query->where(['published' => true], ['published' => 'boolean'])->where($exp); + * ``` + * + * The previous example produces: + * + * `WHERE (id != 100 OR author_id != 1) AND published = 1` + * + * Other Query objects that be used as conditions for any field. + * + * ### Adding conditions in multiple steps: + * + * You can use callable functions to construct complex expressions, functions + * receive as first argument a new QueryExpression object and this query instance + * as second argument. Functions must return an expression object, that will be + * added the list of conditions for the query using the AND operator. + * + * ``` + * $query + * ->where(['title !=' => 'Hello World']) + * ->where(function ($exp, $query) { + * $or = $exp->or_(['id' => 1]); + * $and = $exp->and_(['id >' => 2, 'id <' => 10]); + * return $or->add($and); + * }); + * ``` + * + * * The previous example produces: + * + * `WHERE title != 'Hello World' AND (id = 1 OR (id > 2 AND id < 10))` + * + * ### Conditions as strings: + * + * ``` + * $query->where(['articles.author_id = authors.id', 'modified IS NULL']); + * ``` + * + * The previous example produces: + * + * `WHERE articles.author_id = authors.id AND modified IS NULL` + * + * Please note that when using the array notation or the expression objects, all + * values will be correctly quoted and transformed to the correspondent database + * data type automatically for you, thus securing your application from SQL injections. + * If you use string conditions make sure that your values are correctly quoted. + * The safest thing you can do is to never use string conditions. + * + * @param string|array|callable|null $conditions The conditions to filter on. + * @param array $types associative array of type names used to bind values to query + * @param bool $overwrite whether to reset conditions with passed list or not + * @return $this + */ + public function where($conditions = null, $types = [], $overwrite = false); +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/QueryTrait.php b/app/vendor/cakephp/cakephp/src/Datasource/QueryTrait.php new file mode 100644 index 000000000..286ece9f6 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/QueryTrait.php @@ -0,0 +1,587 @@ +getRepository(); + } + + $this->_repository = $table; + + return $this; + } + + /** + * Returns the default table object that will be used by this query, + * that is, the table that will appear in the from clause. + * + * @return \Cake\Datasource\RepositoryInterface + */ + public function getRepository() + { + return $this->_repository; + } + + /** + * Set the result set for a query. + * + * Setting the resultset of a query will make execute() a no-op. Instead + * of executing the SQL query and fetching results, the ResultSet provided to this + * method will be returned. + * + * This method is most useful when combined with results stored in a persistent cache. + * + * @param \Cake\Datasource\ResultSetInterface $results The results this query should return. + * @return $this + */ + public function setResult($results) + { + $this->_results = $results; + + return $this; + } + + /** + * Executes this query and returns a results iterator. This function is required + * for implementing the IteratorAggregate interface and allows the query to be + * iterated without having to call execute() manually, thus making it look like + * a result set instead of the query itself. + * + * @return \Iterator + */ + public function getIterator() + { + return $this->all(); + } + + /** + * Enable result caching for this query. + * + * If a query has caching enabled, it will do the following when executed: + * + * - Check the cache for $key. If there are results no SQL will be executed. + * Instead the cached results will be returned. + * - When the cached data is stale/missing the result set will be cached as the query + * is executed. + * + * ### Usage + * + * ``` + * // Simple string key + config + * $query->cache('my_key', 'db_results'); + * + * // Function to generate key. + * $query->cache(function ($q) { + * $key = serialize($q->clause('select')); + * $key .= serialize($q->clause('where')); + * return md5($key); + * }); + * + * // Using a pre-built cache engine. + * $query->cache('my_key', $engine); + * + * // Disable caching + * $query->cache(false); + * ``` + * + * @param false|string|\Closure $key Either the cache key or a function to generate the cache key. + * When using a function, this query instance will be supplied as an argument. + * @param string|\Cake\Cache\CacheEngine $config Either the name of the cache config to use, or + * a cache config instance. + * @return $this + */ + public function cache($key, $config = 'default') + { + if ($key === false) { + $this->_cache = null; + + return $this; + } + $this->_cache = new QueryCacher($key, $config); + + return $this; + } + + /** + * Returns the current configured query `_eagerLoaded` value + * + * @return bool + */ + public function isEagerLoaded() + { + return $this->_eagerLoaded; + } + + /** + * Sets the query instance to be an eager loaded query. If no argument is + * passed, the current configured query `_eagerLoaded` value is returned. + * + * @deprecated 3.5.0 Use isEagerLoaded() for the getter part instead. + * @param bool|null $value Whether or not to eager load. + * @return $this|bool + */ + public function eagerLoaded($value = null) + { + if ($value === null) { + deprecationWarning( + 'Using ' . get_called_class() . '::eagerLoaded() as a getter is deprecated. ' . + 'Use isEagerLoaded() instead.' + ); + + return $this->_eagerLoaded; + } + $this->_eagerLoaded = $value; + + return $this; + } + + /** + * Returns a key => value array representing a single aliased field + * that can be passed directly to the select() method. + * The key will contain the alias and the value the actual field name. + * + * If the field is already aliased, then it will not be changed. + * If no $alias is passed, the default table for this query will be used. + * + * @param string $field The field to alias + * @param string|null $alias the alias used to prefix the field + * @return array + */ + public function aliasField($field, $alias = null) + { + $namespaced = strpos($field, '.') !== false; + $aliasedField = $field; + + if ($namespaced) { + list($alias, $field) = explode('.', $field); + } + + if (!$alias) { + $alias = $this->getRepository()->getAlias(); + } + + $key = sprintf('%s__%s', $alias, $field); + if (!$namespaced) { + $aliasedField = $alias . '.' . $field; + } + + return [$key => $aliasedField]; + } + + /** + * Runs `aliasField()` for each field in the provided list and returns + * the result under a single array. + * + * @param array $fields The fields to alias + * @param string|null $defaultAlias The default alias + * @return array + */ + public function aliasFields($fields, $defaultAlias = null) + { + $aliased = []; + foreach ($fields as $alias => $field) { + if (is_numeric($alias) && is_string($field)) { + $aliased += $this->aliasField($field, $defaultAlias); + continue; + } + $aliased[$alias] = $field; + } + + return $aliased; + } + + /** + * Fetch the results for this query. + * + * Will return either the results set through setResult(), or execute this query + * and return the ResultSetDecorator object ready for streaming of results. + * + * ResultSetDecorator is a traversable object that implements the methods found + * on Cake\Collection\Collection. + * + * @return \Cake\Datasource\ResultSetInterface + */ + public function all() + { + if ($this->_results !== null) { + return $this->_results; + } + + if ($this->_cache) { + $results = $this->_cache->fetch($this); + } + if (!isset($results)) { + $results = $this->_decorateResults($this->_execute()); + if ($this->_cache) { + $this->_cache->store($this, $results); + } + } + $this->_results = $results; + + return $this->_results; + } + + /** + * Returns an array representation of the results after executing the query. + * + * @return array + */ + public function toArray() + { + return $this->all()->toArray(); + } + + /** + * Register a new MapReduce routine to be executed on top of the database results + * Both the mapper and caller callable should be invokable objects. + * + * The MapReduce routing will only be run when the query is executed and the first + * result is attempted to be fetched. + * + * If the first argument is set to null, it will return the list of previously + * registered map reduce routines. This is deprecated as of 3.6.0 - use getMapReducers() instead. + * + * If the third argument is set to true, it will erase previous map reducers + * and replace it with the arguments passed. + * + * @param callable|null $mapper The mapper callable. + * @param callable|null $reducer The reducing function. + * @param bool $overwrite Set to true to overwrite existing map + reduce functions. + * @return $this|array + * @see \Cake\Collection\Iterator\MapReduce for details on how to use emit data to the map reducer. + */ + public function mapReduce(callable $mapper = null, callable $reducer = null, $overwrite = false) + { + if ($overwrite) { + $this->_mapReduce = []; + } + if ($mapper === null) { + if (!$overwrite) { + deprecationWarning( + 'Using QueryTrait::mapReduce() as a getter is deprecated. ' . + 'Use getMapReducers() instead.' + ); + } + + return $this->_mapReduce; + } + $this->_mapReduce[] = compact('mapper', 'reducer'); + + return $this; + } + + /** + * Returns the list of previously registered map reduce routines. + * + * @return array + */ + public function getMapReducers() + { + return $this->_mapReduce; + } + + /** + * Registers a new formatter callback function that is to be executed when trying + * to fetch the results from the database. + * + * Formatting callbacks will get a first parameter, an object implementing + * `\Cake\Collection\CollectionInterface`, that can be traversed and modified at will. + * + * Callbacks are required to return an iterator object, which will be used as + * the return value for this query's result. Formatter functions are applied + * after all the `MapReduce` routines for this query have been executed. + * + * If the first argument is set to null, it will return the list of previously + * registered format routines. This is deprecated as of 3.6.0 - use getResultFormatters() instead. + * + * If the second argument is set to true, it will erase previous formatters + * and replace them with the passed first argument. + * + * ### Example: + * + * ``` + * // Return all results from the table indexed by id + * $query->select(['id', 'name'])->formatResults(function ($results) { + * return $results->indexBy('id'); + * }); + * + * // Add a new column to the ResultSet + * $query->select(['name', 'birth_date'])->formatResults(function ($results) { + * return $results->map(function ($row) { + * $row['age'] = $row['birth_date']->diff(new DateTime)->y; + * return $row; + * }); + * }); + * ``` + * + * @param callable|null $formatter The formatting callable. + * @param bool|int $mode Whether or not to overwrite, append or prepend the formatter. + * @return $this|array + */ + public function formatResults(callable $formatter = null, $mode = 0) + { + if ($mode === self::OVERWRITE) { + $this->_formatters = []; + } + if ($formatter === null) { + if ($mode !== self::OVERWRITE) { + deprecationWarning( + 'Using QueryTrait::formatResults() as a getter is deprecated. ' . + 'Use getResultFormatters() instead.' + ); + } + + return $this->_formatters; + } + + if ($mode === self::PREPEND) { + array_unshift($this->_formatters, $formatter); + + return $this; + } + + $this->_formatters[] = $formatter; + + return $this; + } + + /** + * Returns the list of previously registered format routines. + * + * @return array + */ + public function getResultFormatters() + { + return $this->_formatters; + } + + /** + * Returns the first result out of executing this query, if the query has not been + * executed before, it will set the limit clause to 1 for performance reasons. + * + * ### Example: + * + * ``` + * $singleUser = $query->select(['id', 'username'])->first(); + * ``` + * + * @return \Cake\Datasource\EntityInterface|array|null The first result from the ResultSet. + */ + public function first() + { + if ($this->_dirty) { + $this->limit(1); + } + + return $this->all()->first(); + } + + /** + * Get the first result from the executing query or raise an exception. + * + * @throws \Cake\Datasource\Exception\RecordNotFoundException When there is no first record. + * @return \Cake\Datasource\EntityInterface|array The first result from the ResultSet. + */ + public function firstOrFail() + { + $entity = $this->first(); + if (!$entity) { + throw new RecordNotFoundException(sprintf( + 'Record not found in table "%s"', + $this->getRepository()->getTable() + )); + } + + return $entity; + } + + /** + * Returns an array with the custom options that were applied to this query + * and that were not already processed by another method in this class. + * + * ### Example: + * + * ``` + * $query->applyOptions(['doABarrelRoll' => true, 'fields' => ['id', 'name']); + * $query->getOptions(); // Returns ['doABarrelRoll' => true] + * ``` + * + * @see \Cake\Datasource\QueryInterface::applyOptions() to read about the options that will + * be processed by this class and not returned by this function + * @return array + */ + public function getOptions() + { + return $this->_options; + } + + /** + * Enables calling methods from the result set as if they were from this class + * + * @param string $method the method to call + * @param array $arguments list of arguments for the method to call + * @return mixed + * @throws \BadMethodCallException if no such method exists in result set + */ + public function __call($method, $arguments) + { + $resultSetClass = $this->_decoratorClass(); + if (in_array($method, get_class_methods($resultSetClass))) { + $results = $this->all(); + + return $results->$method(...$arguments); + } + throw new BadMethodCallException( + sprintf('Unknown method "%s"', $method) + ); + } + + /** + * Populates or adds parts to current query clauses using an array. + * This is handy for passing all query clauses at once. + * + * @param array $options the options to be applied + * @return $this + */ + abstract public function applyOptions(array $options); + + /** + * Executes this query and returns a traversable object containing the results + * + * @return \Traversable + */ + abstract protected function _execute(); + + /** + * Decorates the results iterator with MapReduce routines and formatters + * + * @param \Traversable $result Original results + * @return \Cake\Datasource\ResultSetInterface + */ + protected function _decorateResults($result) + { + $decorator = $this->_decoratorClass(); + foreach ($this->_mapReduce as $functions) { + $result = new MapReduce($result, $functions['mapper'], $functions['reducer']); + } + + if (!empty($this->_mapReduce)) { + $result = new $decorator($result); + } + + foreach ($this->_formatters as $formatter) { + $result = $formatter($result); + } + + if (!empty($this->_formatters) && !($result instanceof $decorator)) { + $result = new $decorator($result); + } + + return $result; + } + + /** + * Returns the name of the class to be used for decorating results + * + * @return string + */ + protected function _decoratorClass() + { + return ResultSetDecorator::class; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/README.md b/app/vendor/cakephp/cakephp/src/Datasource/README.md new file mode 100644 index 000000000..9eae4c035 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/README.md @@ -0,0 +1,82 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/datasource.svg?style=flat-square)](https://packagist.org/packages/cakephp/datasource) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# CakePHP Datasource Library + +This library contains interfaces for implementing Repositories and Entities using any data source, +a class for managing connections to datasources and traits to help you quickly implement the +interfaces provided by this package. + +## Repositories + +A repository is a class capable of interfacing with a data source using operations such as +`find`, `save` and `delete` by using intermediate query objects for expressing commands to +the data store and returning Entities as the single result unit of such system. + +In the case of a Relational database, a Repository would be a `Table`, which can be return single +or multiple `Entity` objects by using a `Query`. + +This library exposes the following interfaces for creating a system that implements the +repository pattern and is compatible with the CakePHP framework: + +* `RepositoryInterface` - Describes the methods for a base repository class. +* `EntityInterface` - Describes the methods for a single result object. +* `ResultSetInterface` - Represents the idea of a collection of Entities as a result of a query. + +Additionally, this package provides a few traits and classes you can use in your own implementations: + +* `EntityTrait` - Contains the default implementation for the `EntityInterface`. +* `QueryTrait` - Exposes the methods for creating a query object capable of returning decoratable collections. +* `ResultSetDecorator` - Decorates any traversable object so it complies with `ResultSetInterface`. + + +## Connections + +This library contains a couple of utility classes meant to create and manage +connection objects. Connections are typically used in repositories for +interfacing with the actual data source system. + +The `ConnectionManager` class acts as a registry to access database connections +your application has. It provides a place that other objects can get references +to existing connections. Creating connections with the `ConnectionManager` is +easy: + +```php +use Cake\Datasource\ConnectionManager; + +ConnectionManager::config('master', [ + 'className' => 'MyApp\Connections\CustomConnection', + 'param1' => 'value', + 'param2' => 'another value' +]); + +ConnectionManager::config('slave', [ + 'className' => 'MyApp\Connections\CustomConnection', + 'param1' => 'different value', + 'param2' => 'another value' +]); +``` + +When requested, the `ConnectionManager` will instantiate +`MyApp\Connections\CustomConnection` by passing `param1` and `param2` inside an +array as the first argument of the constructor. + +Once configured connections can be fetched using `ConnectionManager::get()`. +This method will construct and load a connection if it has not been built +before, or return the existing known connection: + +```php +use Cake\Datasource\ConnectionManager; +$conn = ConnectionManager::get('master'); +``` + +It is also possible to store connection objects by passing the instance directly to the manager: + +```php +use Cake\Datasource\ConnectionManager; +$conn = ConnectionManager::config('other', $connectionInstance); +``` + +## Documentation + +Please make sure you check the [official API documentation](https://api.cakephp.org/3.x/namespace-Cake.Datasource.html) diff --git a/app/vendor/cakephp/cakephp/src/Datasource/RepositoryInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/RepositoryInterface.php new file mode 100644 index 000000000..8d09c0dd0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/RepositoryInterface.php @@ -0,0 +1,222 @@ +get($id); + * + * $article = $articles->get($id, ['contain' => ['Comments]]); + * ``` + * + * @param mixed $primaryKey primary key value to find + * @param array|\ArrayAccess $options options accepted by `Table::find()` + * @throws \Cake\Datasource\Exception\RecordNotFoundException if the record with such id + * could not be found + * @return \Cake\Datasource\EntityInterface + * @see \Cake\Datasource\RepositoryInterface::find() + */ + public function get($primaryKey, $options = []); + + /** + * Creates a new Query instance for this repository + * + * @return \Cake\Datasource\QueryInterface + */ + public function query(); + + /** + * Update all matching records. + * + * Sets the $fields to the provided values based on $conditions. + * This method will *not* trigger beforeSave/afterSave events. If you need those + * first load a collection of records and update them. + * + * @param string|array|callable|\Cake\Database\Expression\QueryExpression $fields A hash of field => new value. + * @param mixed $conditions Conditions to be used, accepts anything Query::where() + * can take. + * @return int Count Returns the affected rows. + */ + public function updateAll($fields, $conditions); + + /** + * Deletes all records matching the provided conditions. + * + * This method will *not* trigger beforeDelete/afterDelete events. If you + * need those first load a collection of records and delete them. + * + * This method will *not* execute on associations' `cascade` attribute. You should + * use database foreign keys + ON CASCADE rules if you need cascading deletes combined + * with this method. + * + * @param mixed $conditions Conditions to be used, accepts anything Query::where() + * can take. + * @return int Returns the number of affected rows. + * @see \Cake\Datasource\RepositoryInterface::delete() + */ + public function deleteAll($conditions); + + /** + * Returns true if there is any record in this repository matching the specified + * conditions. + * + * @param array|\ArrayAccess $conditions list of conditions to pass to the query + * @return bool + */ + public function exists($conditions); + + /** + * Persists an entity based on the fields that are marked as dirty and + * returns the same entity after a successful save or false in case + * of any error. + * + * @param \Cake\Datasource\EntityInterface $entity the entity to be saved + * @param array|\ArrayAccess $options The options to use when saving. + * @return \Cake\Datasource\EntityInterface|false + */ + public function save(EntityInterface $entity, $options = []); + + /** + * Delete a single entity. + * + * Deletes an entity and possibly related associations from the database + * based on the 'dependent' option used when defining the association. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to remove. + * @param array|\ArrayAccess $options The options for the delete. + * @return bool success + */ + public function delete(EntityInterface $entity, $options = []); + + /** + * Create a new entity + associated entities from an array. + * + * This is most useful when hydrating request data back into entities. + * For example, in your controller code: + * + * ``` + * $article = $this->Articles->newEntity($this->request->getData()); + * ``` + * + * The hydrated entity will correctly do an insert/update based + * on the primary key data existing in the database when the entity + * is saved. Until the entity is saved, it will be a detached record. + * + * @param array|null $data The data to build an entity with. + * @param array $options A list of options for the object hydration. + * @return \Cake\Datasource\EntityInterface + */ + public function newEntity($data = null, array $options = []); + + /** + * Create a list of entities + associated entities from an array. + * + * This is most useful when hydrating request data back into entities. + * For example, in your controller code: + * + * ``` + * $articles = $this->Articles->newEntities($this->request->getData()); + * ``` + * + * The hydrated entities can then be iterated and saved. + * + * @param array $data The data to build an entity with. + * @param array $options A list of options for the objects hydration. + * @return \Cake\Datasource\EntityInterface[] An array of hydrated records. + */ + public function newEntities(array $data, array $options = []); + + /** + * Merges the passed `$data` into `$entity` respecting the accessible + * fields configured on the entity. Returns the same entity after being + * altered. + * + * This is most useful when editing an existing entity using request data: + * + * ``` + * $article = $this->Articles->patchEntity($article, $this->request->getData()); + * ``` + * + * @param \Cake\Datasource\EntityInterface $entity the entity that will get the + * data merged in + * @param array $data key value list of fields to be merged into the entity + * @param array $options A list of options for the object hydration. + * @return \Cake\Datasource\EntityInterface + */ + public function patchEntity(EntityInterface $entity, array $data, array $options = []); + + /** + * Merges each of the elements passed in `$data` into the entities + * found in `$entities` respecting the accessible fields configured on the entities. + * Merging is done by matching the primary key in each of the elements in `$data` + * and `$entities`. + * + * This is most useful when editing a list of existing entities using request data: + * + * ``` + * $article = $this->Articles->patchEntities($articles, $this->request->getData()); + * ``` + * + * @param \Cake\Datasource\EntityInterface[]|\Traversable $entities the entities that will get the + * data merged in + * @param array $data list of arrays to be merged into the entities + * @param array $options A list of options for the objects hydration. + * @return \Cake\Datasource\EntityInterface[] + */ + public function patchEntities($entities, array $data, array $options = []); +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/ResultSetDecorator.php b/app/vendor/cakephp/cakephp/src/Datasource/ResultSetDecorator.php new file mode 100644 index 000000000..6d6f626c1 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/ResultSetDecorator.php @@ -0,0 +1,44 @@ +getInnerIterator() instanceof Countable) { + return $this->getInnerIterator()->count(); + } + + return count($this->toArray()); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/ResultSetInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/ResultSetInterface.php new file mode 100644 index 000000000..f6166e08b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/ResultSetInterface.php @@ -0,0 +1,26 @@ +rule = $rule; + $this->name = $name; + $this->options = $options; + } + + /** + * Set options for the rule invocation. + * + * Old options will be merged with the new ones. + * + * @param array $options The options to set. + * @return $this + */ + public function setOptions(array $options) + { + $this->options = $options + $this->options; + + return $this; + } + + /** + * Set the rule name. + * + * Only truthy names will be set. + * + * @param string $name The name to set. + * @return $this + */ + public function setName($name) + { + if ($name) { + $this->name = $name; + } + + return $this; + } + + /** + * Invoke the rule. + * + * @param \Cake\Datasource\EntityInterface $entity The entity the rule + * should apply to. + * @param array $scope The rule's scope/options. + * @return bool Whether or not the rule passed. + */ + public function __invoke($entity, $scope) + { + $rule = $this->rule; + $pass = $rule($entity, $this->options + $scope); + if ($pass === true || empty($this->options['errorField'])) { + return $pass === true; + } + + $message = 'invalid'; + if (isset($this->options['message'])) { + $message = $this->options['message']; + } + if (is_string($pass)) { + $message = $pass; + } + if ($this->name) { + $message = [$this->name => $message]; + } else { + $message = [$message]; + } + $errorField = $this->options['errorField']; + $entity->setError($errorField, $message); + + if ($entity instanceof InvalidPropertyInterface && isset($entity->{$errorField})) { + $invalidValue = $entity->{$errorField}; + $entity->setInvalidField($errorField, $invalidValue); + } + + return $pass === true; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/RulesAwareTrait.php b/app/vendor/cakephp/cakephp/src/Datasource/RulesAwareTrait.php new file mode 100644 index 000000000..98c161dad --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/RulesAwareTrait.php @@ -0,0 +1,117 @@ +rulesChecker(); + $options = $options ?: new ArrayObject(); + $options = is_array($options) ? new ArrayObject($options) : $options; + $hasEvents = ($this instanceof EventDispatcherInterface); + + if ($hasEvents) { + $event = $this->dispatchEvent( + 'Model.beforeRules', + compact('entity', 'options', 'operation') + ); + if ($event->isStopped()) { + return $event->getResult(); + } + } + + $result = $rules->check($entity, $operation, $options->getArrayCopy()); + + if ($hasEvents) { + $event = $this->dispatchEvent( + 'Model.afterRules', + compact('entity', 'options', 'result', 'operation') + ); + + if ($event->isStopped()) { + return $event->getResult(); + } + } + + return $result; + } + + /** + * Returns the RulesChecker for this instance. + * + * A RulesChecker object is used to test an entity for validity + * on rules that may involve complex logic or data that + * needs to be fetched from relevant datasources. + * + * @see \Cake\Datasource\RulesChecker + * @return \Cake\Datasource\RulesChecker + */ + public function rulesChecker() + { + if ($this->_rulesChecker !== null) { + return $this->_rulesChecker; + } + $class = defined('static::RULES_CLASS') ? static::RULES_CLASS : 'Cake\Datasource\RulesChecker'; + $this->_rulesChecker = $this->buildRules(new $class(['repository' => $this])); + $this->dispatchEvent('Model.buildRules', ['rules' => $this->_rulesChecker]); + + return $this->_rulesChecker; + } + + /** + * Returns a RulesChecker object after modifying the one that was supplied. + * + * Subclasses should override this method in order to initialize the rules to be applied to + * entities saved by this instance. + * + * @param \Cake\Datasource\RulesChecker $rules The rules object to be modified. + * @return \Cake\Datasource\RulesChecker + */ + public function buildRules(RulesChecker $rules) + { + return $rules; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/RulesChecker.php b/app/vendor/cakephp/cakephp/src/Datasource/RulesChecker.php new file mode 100644 index 000000000..f0bfedef9 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/RulesChecker.php @@ -0,0 +1,328 @@ +_options = $options; + $this->_useI18n = function_exists('__d'); + } + + /** + * Adds a rule that will be applied to the entity both on create and update + * operations. + * + * ### Options + * + * The options array accept the following special keys: + * + * - `errorField`: The name of the entity field that will be marked as invalid + * if the rule does not pass. + * - `message`: The error message to set to `errorField` if the rule does not pass. + * + * @param callable $rule A callable function or object that will return whether + * the entity is valid or not. + * @param string|null $name The alias for a rule. + * @param array $options List of extra options to pass to the rule callable as + * second argument. + * @return $this + */ + public function add(callable $rule, $name = null, array $options = []) + { + $this->_rules[] = $this->_addError($rule, $name, $options); + + return $this; + } + + /** + * Adds a rule that will be applied to the entity on create operations. + * + * ### Options + * + * The options array accept the following special keys: + * + * - `errorField`: The name of the entity field that will be marked as invalid + * if the rule does not pass. + * - `message`: The error message to set to `errorField` if the rule does not pass. + * + * @param callable $rule A callable function or object that will return whether + * the entity is valid or not. + * @param string|null $name The alias for a rule. + * @param array $options List of extra options to pass to the rule callable as + * second argument. + * @return $this + */ + public function addCreate(callable $rule, $name = null, array $options = []) + { + $this->_createRules[] = $this->_addError($rule, $name, $options); + + return $this; + } + + /** + * Adds a rule that will be applied to the entity on update operations. + * + * ### Options + * + * The options array accept the following special keys: + * + * - `errorField`: The name of the entity field that will be marked as invalid + * if the rule does not pass. + * - `message`: The error message to set to `errorField` if the rule does not pass. + * + * @param callable $rule A callable function or object that will return whether + * the entity is valid or not. + * @param string|null $name The alias for a rule. + * @param array $options List of extra options to pass to the rule callable as + * second argument. + * @return $this + */ + public function addUpdate(callable $rule, $name = null, array $options = []) + { + $this->_updateRules[] = $this->_addError($rule, $name, $options); + + return $this; + } + + /** + * Adds a rule that will be applied to the entity on delete operations. + * + * ### Options + * + * The options array accept the following special keys: + * + * - `errorField`: The name of the entity field that will be marked as invalid + * if the rule does not pass. + * - `message`: The error message to set to `errorField` if the rule does not pass. + * + * @param callable $rule A callable function or object that will return whether + * the entity is valid or not. + * @param string|null $name The alias for a rule. + * @param array $options List of extra options to pass to the rule callable as + * second argument. + * @return $this + */ + public function addDelete(callable $rule, $name = null, array $options = []) + { + $this->_deleteRules[] = $this->_addError($rule, $name, $options); + + return $this; + } + + /** + * Runs each of the rules by passing the provided entity and returns true if all + * of them pass. The rules to be applied are depended on the $mode parameter which + * can only be RulesChecker::CREATE, RulesChecker::UPDATE or RulesChecker::DELETE + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @param string $mode Either 'create, 'update' or 'delete'. + * @param array $options Extra options to pass to checker functions. + * @return bool + * @throws \InvalidArgumentException if an invalid mode is passed. + */ + public function check(EntityInterface $entity, $mode, array $options = []) + { + if ($mode === self::CREATE) { + return $this->checkCreate($entity, $options); + } + + if ($mode === self::UPDATE) { + return $this->checkUpdate($entity, $options); + } + + if ($mode === self::DELETE) { + return $this->checkDelete($entity, $options); + } + + throw new InvalidArgumentException('Wrong checking mode: ' . $mode); + } + + /** + * Runs each of the rules by passing the provided entity and returns true if all + * of them pass. The rules selected will be only those specified to be run on 'create' + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @param array $options Extra options to pass to checker functions. + * @return bool + */ + public function checkCreate(EntityInterface $entity, array $options = []) + { + return $this->_checkRules($entity, $options, array_merge($this->_rules, $this->_createRules)); + } + + /** + * Runs each of the rules by passing the provided entity and returns true if all + * of them pass. The rules selected will be only those specified to be run on 'update' + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @param array $options Extra options to pass to checker functions. + * @return bool + */ + public function checkUpdate(EntityInterface $entity, array $options = []) + { + return $this->_checkRules($entity, $options, array_merge($this->_rules, $this->_updateRules)); + } + + /** + * Runs each of the rules by passing the provided entity and returns true if all + * of them pass. The rules selected will be only those specified to be run on 'delete' + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @param array $options Extra options to pass to checker functions. + * @return bool + */ + public function checkDelete(EntityInterface $entity, array $options = []) + { + return $this->_checkRules($entity, $options, $this->_deleteRules); + } + + /** + * Used by top level functions checkDelete, checkCreate and checkUpdate, this function + * iterates an array containing the rules to be checked and checks them all. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity. + * @param array $options Extra options to pass to checker functions. + * @param array $rules The list of rules that must be checked. + * @return bool + */ + protected function _checkRules(EntityInterface $entity, array $options = [], array $rules = []) + { + $success = true; + $options += $this->_options; + foreach ($rules as $rule) { + $success = $rule($entity, $options) && $success; + } + + return $success; + } + + /** + * Utility method for decorating any callable so that if it returns false, the correct + * property in the entity is marked as invalid. + * + * @param callable $rule The rule to decorate + * @param string $name The alias for a rule. + * @param array $options The options containing the error message and field. + * @return callable + */ + protected function _addError($rule, $name, $options) + { + if (is_array($name)) { + $options = $name; + $name = null; + } + + if (!($rule instanceof RuleInvoker)) { + $rule = new RuleInvoker($rule, $name, $options); + } else { + $rule->setOptions($options)->setName($name); + } + + return $rule; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Datasource/SchemaInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/SchemaInterface.php new file mode 100644 index 000000000..997ffa1e4 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Datasource/SchemaInterface.php @@ -0,0 +1,165 @@ +=5.6.0", + "cakephp/core": "^3.6.0" + }, + "suggest": { + "cakephp/utility": "If you decide to use EntityTrait.", + "cakephp/collection": "If you decide to use ResultSetInterface.", + "cakephp/cache": "If you decide to use Query caching." + }, + "autoload": { + "psr-4": { + "Cake\\Datasource\\": "." + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Error/BaseErrorHandler.php b/app/vendor/cakephp/cakephp/src/Error/BaseErrorHandler.php new file mode 100644 index 000000000..b8a51a0de --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Error/BaseErrorHandler.php @@ -0,0 +1,427 @@ +_options['errorLevel'])) { + $level = $this->_options['errorLevel']; + } + error_reporting($level); + set_error_handler([$this, 'handleError'], $level); + set_exception_handler([$this, 'wrapAndHandleException']); + register_shutdown_function(function () { + if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && $this->_handled) { + return; + } + $megabytes = Configure::read('Error.extraFatalErrorMemory'); + if ($megabytes === null) { + $megabytes = 4; + } + if ($megabytes > 0) { + $this->increaseMemoryLimit($megabytes * 1024); + } + $error = error_get_last(); + if (!is_array($error)) { + return; + } + $fatals = [ + E_USER_ERROR, + E_ERROR, + E_PARSE, + ]; + if (!in_array($error['type'], $fatals, true)) { + return; + } + $this->handleFatalError( + $error['type'], + $error['message'], + $error['file'], + $error['line'] + ); + }); + } + + /** + * Set as the default error handler by CakePHP. + * + * Use config/error.php to customize or replace this error handler. + * This function will use Debugger to display errors when debug > 0. And + * will log errors to Log, when debug == 0. + * + * You can use the 'errorLevel' option to set what type of errors will be handled. + * Stack traces for errors can be enabled with the 'trace' option. + * + * @param int $code Code of error + * @param string $description Error description + * @param string|null $file File on which error occurred + * @param int|null $line Line that triggered the error + * @param array|null $context Context + * @return bool True if error was handled + */ + public function handleError($code, $description, $file = null, $line = null, $context = null) + { + if (error_reporting() === 0) { + return false; + } + $this->_handled = true; + list($error, $log) = static::mapErrorCode($code); + if ($log === LOG_ERR) { + return $this->handleFatalError($code, $description, $file, $line); + } + $data = [ + 'level' => $log, + 'code' => $code, + 'error' => $error, + 'description' => $description, + 'file' => $file, + 'line' => $line, + ]; + + $debug = Configure::read('debug'); + if ($debug) { + $data += [ + 'context' => $context, + 'start' => 3, + 'path' => Debugger::trimPath($file) + ]; + } + $this->_displayError($data, $debug); + $this->_logError($log, $data); + + return true; + } + + /** + * Checks the passed exception type. If it is an instance of `Error` + * then, it wraps the passed object inside another Exception object + * for backwards compatibility purposes. + * + * @param \Exception|\Error $exception The exception to handle + * @return void + */ + public function wrapAndHandleException($exception) + { + if ($exception instanceof Error) { + $exception = new PHP7ErrorException($exception); + } + $this->handleException($exception); + } + + /** + * Handle uncaught exceptions. + * + * Uses a template method provided by subclasses to display errors in an + * environment appropriate way. + * + * @param \Exception $exception Exception instance. + * @return void + * @throws \Exception When renderer class not found + * @see https://secure.php.net/manual/en/function.set-exception-handler.php + */ + public function handleException(Exception $exception) + { + $this->_displayException($exception); + $this->_logException($exception); + $this->_stop($exception->getCode() ?: 1); + } + + /** + * Stop the process. + * + * Implemented in subclasses that need it. + * + * @param int $code Exit code. + * @return void + */ + protected function _stop($code) + { + // Do nothing. + } + + /** + * Display/Log a fatal error. + * + * @param int $code Code of error + * @param string $description Error description + * @param string $file File on which error occurred + * @param int $line Line that triggered the error + * @return bool + */ + public function handleFatalError($code, $description, $file, $line) + { + $data = [ + 'code' => $code, + 'description' => $description, + 'file' => $file, + 'line' => $line, + 'error' => 'Fatal Error', + ]; + $this->_logError(LOG_ERR, $data); + + $this->handleException(new FatalErrorException($description, 500, $file, $line)); + + return true; + } + + /** + * Increases the PHP "memory_limit" ini setting by the specified amount + * in kilobytes + * + * @param int $additionalKb Number in kilobytes + * @return void + */ + public function increaseMemoryLimit($additionalKb) + { + $limit = ini_get('memory_limit'); + if (!strlen($limit) || $limit === '-1') { + return; + } + $limit = trim($limit); + $units = strtoupper(substr($limit, -1)); + $current = (int)substr($limit, 0, strlen($limit) - 1); + if ($units === 'M') { + $current *= 1024; + $units = 'K'; + } + if ($units === 'G') { + $current = $current * 1024 * 1024; + $units = 'K'; + } + + if ($units === 'K') { + ini_set('memory_limit', ceil($current + $additionalKb) . 'K'); + } + } + + /** + * Log an error. + * + * @param string $level The level name of the log. + * @param array $data Array of error data. + * @return bool + */ + protected function _logError($level, $data) + { + $message = sprintf( + '%s (%s): %s in [%s, line %s]', + $data['error'], + $data['code'], + $data['description'], + $data['file'], + $data['line'] + ); + if (!empty($this->_options['trace'])) { + $trace = Debugger::trace([ + 'start' => 1, + 'format' => 'log' + ]); + + $request = Router::getRequest(); + if ($request) { + $message .= $this->_requestContext($request); + } + $message .= "\nTrace:\n" . $trace . "\n"; + } + $message .= "\n\n"; + + return Log::write($level, $message); + } + + /** + * Handles exception logging + * + * @param \Exception $exception Exception instance. + * @return bool + */ + protected function _logException(Exception $exception) + { + $config = $this->_options; + $unwrapped = $exception instanceof PHP7ErrorException ? + $exception->getError() : + $exception; + + if (empty($config['log'])) { + return false; + } + + if (!empty($config['skipLog'])) { + foreach ((array)$config['skipLog'] as $class) { + if ($unwrapped instanceof $class) { + return false; + } + } + } + + return Log::error($this->_getMessage($exception)); + } + + /** + * Get the request context for an error/exception trace. + * + * @param \Cake\Http\ServerRequest $request The request to read from. + * @return string + */ + protected function _requestContext($request) + { + $message = "\nRequest URL: " . $request->getRequestTarget(); + + $referer = $request->getEnv('HTTP_REFERER'); + if ($referer) { + $message .= "\nReferer URL: " . $referer; + } + $clientIp = $request->clientIp(); + if ($clientIp && $clientIp !== '::1') { + $message .= "\nClient IP: " . $clientIp; + } + + return $message; + } + + /** + * Generates a formatted error message + * + * @param \Exception $exception Exception instance + * @return string Formatted message + */ + protected function _getMessage(Exception $exception) + { + $exception = $exception instanceof PHP7ErrorException ? + $exception->getError() : + $exception; + $config = $this->_options; + $message = sprintf( + '[%s] %s in %s on line %s', + get_class($exception), + $exception->getMessage(), + $exception->getFile(), + $exception->getLine() + ); + $debug = Configure::read('debug'); + + if ($debug && method_exists($exception, 'getAttributes')) { + $attributes = $exception->getAttributes(); + if ($attributes) { + $message .= "\nException Attributes: " . var_export($exception->getAttributes(), true); + } + } + + $request = Router::getRequest(); + if ($request) { + $message .= $this->_requestContext($request); + } + + if (!empty($config['trace'])) { + $message .= "\nStack Trace:\n" . $exception->getTraceAsString() . "\n\n"; + } + + return $message; + } + + /** + * Map an error code into an Error word, and log location. + * + * @param int $code Error code to map + * @return array Array of error word, and log location. + */ + public static function mapErrorCode($code) + { + $levelMap = [ + E_PARSE => 'error', + E_ERROR => 'error', + E_CORE_ERROR => 'error', + E_COMPILE_ERROR => 'error', + E_USER_ERROR => 'error', + E_WARNING => 'warning', + E_USER_WARNING => 'warning', + E_COMPILE_WARNING => 'warning', + E_RECOVERABLE_ERROR => 'warning', + E_NOTICE => 'notice', + E_USER_NOTICE => 'notice', + E_STRICT => 'strict', + E_DEPRECATED => 'deprecated', + E_USER_DEPRECATED => 'deprecated', + ]; + $logMap = [ + 'error' => LOG_ERR, + 'warning' => LOG_WARNING, + 'notice' => LOG_NOTICE, + 'strict' => LOG_NOTICE, + 'deprecated' => LOG_NOTICE, + ]; + + $error = $levelMap[$code]; + $log = $logMap[$error]; + + return [ucfirst($error), $log]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Error/Debugger.php b/app/vendor/cakephp/cakephp/src/Error/Debugger.php new file mode 100644 index 000000000..b7b88bf47 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Error/Debugger.php @@ -0,0 +1,966 @@ + [] + ]; + + /** + * A list of errors generated by the application. + * + * @var array + */ + public $errors = []; + + /** + * The current output format. + * + * @var string + */ + protected $_outputFormat = 'js'; + + /** + * Templates used when generating trace or error strings. Can be global or indexed by the format + * value used in $_outputFormat. + * + * @var array + */ + protected $_templates = [ + 'log' => [ + 'trace' => '{:reference} - {:path}, line {:line}', + 'error' => '{:error} ({:code}): {:description} in [{:file}, line {:line}]' + ], + 'js' => [ + 'error' => '', + 'info' => '', + 'trace' => '
{:trace}
', + 'code' => '', + 'context' => '', + 'links' => [], + 'escapeContext' => true, + ], + 'html' => [ + 'trace' => '
Trace 

{:trace}

', + 'context' => '
Context 

{:context}

', + 'escapeContext' => true, + ], + 'txt' => [ + 'error' => "{:error}: {:code} :: {:description} on line {:line} of {:path}\n{:info}", + 'code' => '', + 'info' => '' + ], + 'base' => [ + 'traceLine' => '{:reference} - {:path}, line {:line}', + 'trace' => "Trace:\n{:trace}\n", + 'context' => "Context:\n{:context}\n", + ] + ]; + + /** + * Holds current output data when outputFormat is false. + * + * @var array + */ + protected $_data = []; + + /** + * Constructor. + * + */ + public function __construct() + { + $docRef = ini_get('docref_root'); + + if (empty($docRef) && function_exists('ini_set')) { + ini_set('docref_root', 'https://secure.php.net/'); + } + if (!defined('E_RECOVERABLE_ERROR')) { + define('E_RECOVERABLE_ERROR', 4096); + } + + $e = '
';
+        $e .= '{:error} ({:code}): {:description} ';
+        $e .= '[{:path}, line {:line}]';
+
+        $e .= '';
+        $e .= '
'; + $this->_templates['js']['error'] = $e; + + $t = ''; + $this->_templates['js']['info'] = $t; + + $links = []; + $link = 'Code'; + $links['code'] = $link; + + $link = 'Context'; + $links['context'] = $link; + + $this->_templates['js']['links'] = $links; + + $this->_templates['js']['context'] = '
_templates['js']['context'] .= 'style="display: none;">{:context}
'; + + $this->_templates['js']['code'] = '
_templates['js']['code'] .= 'style="display: none;">{:code}
'; + + $e = '
{:error} ({:code}) : {:description} ';
+        $e .= '[{:path}, line {:line}]
'; + $this->_templates['html']['error'] = $e; + + $this->_templates['html']['context'] = '
Context ';
+        $this->_templates['html']['context'] .= '

{:context}

'; + } + + /** + * Returns a reference to the Debugger singleton object instance. + * + * @param string|null $class Class name. + * @return \Cake\Error\Debugger + */ + public static function getInstance($class = null) + { + static $instance = []; + if (!empty($class)) { + if (!$instance || strtolower($class) !== strtolower(get_class($instance[0]))) { + $instance[0] = new $class(); + } + } + if (!$instance) { + $instance[0] = new Debugger(); + } + + return $instance[0]; + } + + /** + * Read or write configuration options for the Debugger instance. + * + * @param string|array|null $key The key to get/set, or a complete array of configs. + * @param mixed|null $value The value to set. + * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true. + * @return mixed Config value being read, or the object itself on write operations. + * @throws \Cake\Core\Exception\Exception When trying to set a key that is invalid. + */ + public static function configInstance($key = null, $value = null, $merge = true) + { + if (is_array($key) || func_num_args() >= 2) { + return static::getInstance()->setConfig($key, $value, $merge); + } + + return static::getInstance()->getConfig($key); + } + + /** + * Reads the current output masking. + * + * @return array + */ + public static function outputMask() + { + return static::configInstance('outputMask'); + } + + /** + * Sets configurable masking of debugger output by property name and array key names. + * + * ### Example + * + * Debugger::setOutputMask(['password' => '[*************]'); + * + * @param array $value An array where keys are replaced by their values in output. + * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true. + * @return void + */ + public static function setOutputMask(array $value, $merge = true) + { + static::configInstance('outputMask', $value, $merge); + } + + /** + * Recursively formats and outputs the contents of the supplied variable. + * + * @param mixed $var The variable to dump. + * @param int $depth The depth to output to. Defaults to 3. + * @return void + * @see \Cake\Error\Debugger::exportVar() + * @link https://book.cakephp.org/3.0/en/development/debugging.html#outputting-values + */ + public static function dump($var, $depth = 3) + { + pr(static::exportVar($var, $depth)); + } + + /** + * Creates an entry in the log file. The log entry will contain a stack trace from where it was called. + * as well as export the variable using exportVar. By default the log is written to the debug log. + * + * @param mixed $var Variable or content to log. + * @param int|string $level Type of log to use. Defaults to 'debug'. + * @param int $depth The depth to output to. Defaults to 3. + * @return void + */ + public static function log($var, $level = 'debug', $depth = 3) + { + $source = static::trace(['start' => 1]) . "\n"; + Log::write($level, "\n" . $source . static::exportVar($var, $depth)); + } + + /** + * Outputs a stack trace based on the supplied options. + * + * ### Options + * + * - `depth` - The number of stack frames to return. Defaults to 999 + * - `format` - The format you want the return. Defaults to the currently selected format. If + * format is 'array' or 'points' the return will be an array. + * - `args` - Should arguments for functions be shown? If true, the arguments for each method call + * will be displayed. + * - `start` - The stack frame to start generating a trace from. Defaults to 0 + * + * @param array $options Format for outputting stack trace. + * @return mixed Formatted stack trace. + * @link https://book.cakephp.org/3.0/en/development/debugging.html#generating-stack-traces + */ + public static function trace(array $options = []) + { + return Debugger::formatTrace(debug_backtrace(), $options); + } + + /** + * Formats a stack trace based on the supplied options. + * + * ### Options + * + * - `depth` - The number of stack frames to return. Defaults to 999 + * - `format` - The format you want the return. Defaults to the currently selected format. If + * format is 'array' or 'points' the return will be an array. + * - `args` - Should arguments for functions be shown? If true, the arguments for each method call + * will be displayed. + * - `start` - The stack frame to start generating a trace from. Defaults to 0 + * + * @param array|\Exception $backtrace Trace as array or an exception object. + * @param array $options Format for outputting stack trace. + * @return mixed Formatted stack trace. + * @link https://book.cakephp.org/3.0/en/development/debugging.html#generating-stack-traces + */ + public static function formatTrace($backtrace, $options = []) + { + if ($backtrace instanceof Exception) { + $backtrace = $backtrace->getTrace(); + } + $self = Debugger::getInstance(); + $defaults = [ + 'depth' => 999, + 'format' => $self->_outputFormat, + 'args' => false, + 'start' => 0, + 'scope' => null, + 'exclude' => ['call_user_func_array', 'trigger_error'] + ]; + $options = Hash::merge($defaults, $options); + + $count = count($backtrace); + $back = []; + + $_trace = [ + 'line' => '??', + 'file' => '[internal]', + 'class' => null, + 'function' => '[main]' + ]; + + for ($i = $options['start']; $i < $count && $i < $options['depth']; $i++) { + $trace = $backtrace[$i] + ['file' => '[internal]', 'line' => '??']; + $signature = $reference = '[main]'; + + if (isset($backtrace[$i + 1])) { + $next = $backtrace[$i + 1] + $_trace; + $signature = $reference = $next['function']; + + if (!empty($next['class'])) { + $signature = $next['class'] . '::' . $next['function']; + $reference = $signature . '('; + if ($options['args'] && isset($next['args'])) { + $args = []; + foreach ($next['args'] as $arg) { + $args[] = Debugger::exportVar($arg); + } + $reference .= implode(', ', $args); + } + $reference .= ')'; + } + } + if (in_array($signature, $options['exclude'])) { + continue; + } + if ($options['format'] === 'points' && $trace['file'] !== '[internal]') { + $back[] = ['file' => $trace['file'], 'line' => $trace['line']]; + } elseif ($options['format'] === 'array') { + $back[] = $trace; + } else { + if (isset($self->_templates[$options['format']]['traceLine'])) { + $tpl = $self->_templates[$options['format']]['traceLine']; + } else { + $tpl = $self->_templates['base']['traceLine']; + } + $trace['path'] = static::trimPath($trace['file']); + $trace['reference'] = $reference; + unset($trace['object'], $trace['args']); + $back[] = Text::insert($tpl, $trace, ['before' => '{:', 'after' => '}']); + } + } + + if ($options['format'] === 'array' || $options['format'] === 'points') { + return $back; + } + + return implode("\n", $back); + } + + /** + * Shortens file paths by replacing the application base path with 'APP', and the CakePHP core + * path with 'CORE'. + * + * @param string $path Path to shorten. + * @return string Normalized path + */ + public static function trimPath($path) + { + if (defined('APP') && strpos($path, APP) === 0) { + return str_replace(APP, 'APP/', $path); + } + if (defined('CAKE_CORE_INCLUDE_PATH') && strpos($path, CAKE_CORE_INCLUDE_PATH) === 0) { + return str_replace(CAKE_CORE_INCLUDE_PATH, 'CORE', $path); + } + if (defined('ROOT') && strpos($path, ROOT) === 0) { + return str_replace(ROOT, 'ROOT', $path); + } + + return $path; + } + + /** + * Grabs an excerpt from a file and highlights a given line of code. + * + * Usage: + * + * ``` + * Debugger::excerpt('/path/to/file', 100, 4); + * ``` + * + * The above would return an array of 8 items. The 4th item would be the provided line, + * and would be wrapped in ``. All of the lines + * are processed with highlight_string() as well, so they have basic PHP syntax highlighting + * applied. + * + * @param string $file Absolute path to a PHP file. + * @param int $line Line number to highlight. + * @param int $context Number of lines of context to extract above and below $line. + * @return array Set of lines highlighted + * @see https://secure.php.net/highlight_string + * @link https://book.cakephp.org/3.0/en/development/debugging.html#getting-an-excerpt-from-a-file + */ + public static function excerpt($file, $line, $context = 2) + { + $lines = []; + if (!file_exists($file)) { + return []; + } + $data = file_get_contents($file); + if (empty($data)) { + return $lines; + } + if (strpos($data, "\n") !== false) { + $data = explode("\n", $data); + } + $line--; + if (!isset($data[$line])) { + return $lines; + } + for ($i = $line - $context; $i < $line + $context + 1; $i++) { + if (!isset($data[$i])) { + continue; + } + $string = str_replace(["\r\n", "\n"], '', static::_highlight($data[$i])); + if ($i == $line) { + $lines[] = '' . $string . ''; + } else { + $lines[] = $string; + } + } + + return $lines; + } + + /** + * Wraps the highlight_string function in case the server API does not + * implement the function as it is the case of the HipHop interpreter + * + * @param string $str The string to convert. + * @return string + */ + protected static function _highlight($str) + { + if (function_exists('hphp_log') || function_exists('hphp_gettid')) { + return htmlentities($str); + } + $added = false; + if (strpos($str, '', '<?php 
'], + '', + $highlight + ); + } + + return $highlight; + } + + /** + * Converts a variable to a string for debug output. + * + * *Note:* The following keys will have their contents + * replaced with `*****`: + * + * - password + * - login + * - host + * - database + * - port + * - prefix + * - schema + * + * This is done to protect database credentials, which could be accidentally + * shown in an error message if CakePHP is deployed in development mode. + * + * @param mixed $var Variable to convert. + * @param int $depth The depth to output to. Defaults to 3. + * @return string Variable as a formatted string + */ + public static function exportVar($var, $depth = 3) + { + return static::_export($var, $depth, 0); + } + + /** + * Protected export function used to keep track of indentation and recursion. + * + * @param mixed $var The variable to dump. + * @param int $depth The remaining depth. + * @param int $indent The current indentation level. + * @return string The dumped variable. + */ + protected static function _export($var, $depth, $indent) + { + switch (static::getType($var)) { + case 'boolean': + return $var ? 'true' : 'false'; + case 'integer': + return '(int) ' . $var; + case 'float': + return '(float) ' . $var; + case 'string': + if (trim($var) === '' && ctype_space($var) === false) { + return "''"; + } + + return "'" . $var . "'"; + case 'array': + return static::_array($var, $depth - 1, $indent + 1); + case 'resource': + return strtolower(gettype($var)); + case 'null': + return 'null'; + case 'unknown': + return 'unknown'; + default: + return static::_object($var, $depth - 1, $indent + 1); + } + } + + /** + * Export an array type object. Filters out keys used in datasource configuration. + * + * The following keys are replaced with ***'s + * + * - password + * - login + * - host + * - database + * - port + * - prefix + * - schema + * + * @param array $var The array to export. + * @param int $depth The current depth, used for recursion tracking. + * @param int $indent The current indentation level. + * @return string Exported array. + */ + protected static function _array(array $var, $depth, $indent) + { + $out = '['; + $break = $end = null; + if (!empty($var)) { + $break = "\n" . str_repeat("\t", $indent); + $end = "\n" . str_repeat("\t", $indent - 1); + } + $vars = []; + + if ($depth >= 0) { + $outputMask = (array)static::outputMask(); + foreach ($var as $key => $val) { + // Sniff for globals as !== explodes in < 5.4 + if ($key === 'GLOBALS' && is_array($val) && isset($val['GLOBALS'])) { + $val = '[recursion]'; + } elseif (array_key_exists($key, $outputMask)) { + $val = (string)$outputMask[$key]; + } elseif ($val !== $var) { + $val = static::_export($val, $depth, $indent); + } + $vars[] = $break . static::exportVar($key) . + ' => ' . + $val; + } + } else { + $vars[] = $break . '[maximum depth reached]'; + } + + return $out . implode(',', $vars) . $end . ']'; + } + + /** + * Handles object to string conversion. + * + * @param object $var Object to convert. + * @param int $depth The current depth, used for tracking recursion. + * @param int $indent The current indentation level. + * @return string + * @see \Cake\Error\Debugger::exportVar() + */ + protected static function _object($var, $depth, $indent) + { + $out = ''; + $props = []; + + $className = get_class($var); + $out .= 'object(' . $className . ') {'; + $break = "\n" . str_repeat("\t", $indent); + $end = "\n" . str_repeat("\t", $indent - 1); + + if ($depth > 0 && method_exists($var, '__debugInfo')) { + try { + return $out . "\n" . + substr(static::_array($var->__debugInfo(), $depth - 1, $indent), 1, -1) . + $end . '}'; + } catch (Exception $e) { + $message = $e->getMessage(); + + return $out . "\n(unable to export object: $message)\n }"; + } + } + + if ($depth > 0) { + $outputMask = (array)static::outputMask(); + $objectVars = get_object_vars($var); + foreach ($objectVars as $key => $value) { + $value = array_key_exists($key, $outputMask) ? $outputMask[$key] : static::_export($value, $depth - 1, $indent); + $props[] = "$key => " . $value; + } + + $ref = new ReflectionObject($var); + + $filters = [ + ReflectionProperty::IS_PROTECTED => 'protected', + ReflectionProperty::IS_PRIVATE => 'private', + ]; + foreach ($filters as $filter => $visibility) { + $reflectionProperties = $ref->getProperties($filter); + foreach ($reflectionProperties as $reflectionProperty) { + $reflectionProperty->setAccessible(true); + $property = $reflectionProperty->getValue($var); + + $value = static::_export($property, $depth - 1, $indent); + $key = $reflectionProperty->name; + $props[] = sprintf( + '[%s] %s => %s', + $visibility, + $key, + array_key_exists($key, $outputMask) ? $outputMask[$key] : $value + ); + } + } + + $out .= $break . implode($break, $props) . $end; + } + $out .= '}'; + + return $out; + } + + /** + * Get the output format for Debugger error rendering. + * + * @return string Returns the current format when getting. + */ + public static function getOutputFormat() + { + return Debugger::getInstance()->_outputFormat; + } + + /** + * Set the output format for Debugger error rendering. + * + * @param string $format The format you want errors to be output as. + * @return void + * @throws \InvalidArgumentException When choosing a format that doesn't exist. + */ + public static function setOutputFormat($format) + { + $self = Debugger::getInstance(); + + if (!isset($self->_templates[$format])) { + throw new InvalidArgumentException('Invalid Debugger output format.'); + } + $self->_outputFormat = $format; + } + + /** + * Get/Set the output format for Debugger error rendering. + * + * @deprecated 3.5.0 Use getOutputFormat()/setOutputFormat() instead. + * @param string|null $format The format you want errors to be output as. + * Leave null to get the current format. + * @return string|null Returns null when setting. Returns the current format when getting. + * @throws \InvalidArgumentException When choosing a format that doesn't exist. + */ + public static function outputAs($format = null) + { + deprecationWarning( + 'Debugger::outputAs() is deprecated. Use Debugger::getOutputFormat()/setOutputFormat() instead.' + ); + $self = Debugger::getInstance(); + if ($format === null) { + return $self->_outputFormat; + } + + if (!isset($self->_templates[$format])) { + throw new InvalidArgumentException('Invalid Debugger output format.'); + } + $self->_outputFormat = $format; + + return null; + } + + /** + * Add an output format or update a format in Debugger. + * + * ``` + * Debugger::addFormat('custom', $data); + * ``` + * + * Where $data is an array of strings that use Text::insert() variable + * replacement. The template vars should be in a `{:id}` style. + * An error formatter can have the following keys: + * + * - 'error' - Used for the container for the error message. Gets the following template + * variables: `id`, `error`, `code`, `description`, `path`, `line`, `links`, `info` + * - 'info' - A combination of `code`, `context` and `trace`. Will be set with + * the contents of the other template keys. + * - 'trace' - The container for a stack trace. Gets the following template + * variables: `trace` + * - 'context' - The container element for the context variables. + * Gets the following templates: `id`, `context` + * - 'links' - An array of HTML links that are used for creating links to other resources. + * Typically this is used to create javascript links to open other sections. + * Link keys, are: `code`, `context`, `help`. See the js output format for an + * example. + * - 'traceLine' - Used for creating lines in the stacktrace. Gets the following + * template variables: `reference`, `path`, `line` + * + * Alternatively if you want to use a custom callback to do all the formatting, you can use + * the callback key, and provide a callable: + * + * ``` + * Debugger::addFormat('custom', ['callback' => [$foo, 'outputError']]; + * ``` + * + * The callback can expect two parameters. The first is an array of all + * the error data. The second contains the formatted strings generated using + * the other template strings. Keys like `info`, `links`, `code`, `context` and `trace` + * will be present depending on the other templates in the format type. + * + * @param string $format Format to use, including 'js' for JavaScript-enhanced HTML, 'html' for + * straight HTML output, or 'txt' for unformatted text. + * @param array $strings Template strings, or a callback to be used for the output format. + * @return array The resulting format string set. + */ + public static function addFormat($format, array $strings) + { + $self = Debugger::getInstance(); + if (isset($self->_templates[$format])) { + if (isset($strings['links'])) { + $self->_templates[$format]['links'] = array_merge( + $self->_templates[$format]['links'], + $strings['links'] + ); + unset($strings['links']); + } + $self->_templates[$format] = $strings + $self->_templates[$format]; + } else { + $self->_templates[$format] = $strings; + } + + return $self->_templates[$format]; + } + + /** + * Takes a processed array of data from an error and displays it in the chosen format. + * + * @param array $data Data to output. + * @return void + */ + public function outputError($data) + { + $defaults = [ + 'level' => 0, + 'error' => 0, + 'code' => 0, + 'description' => '', + 'file' => '', + 'line' => 0, + 'context' => [], + 'start' => 2, + ]; + $data += $defaults; + + $files = static::trace(['start' => $data['start'], 'format' => 'points']); + $code = ''; + $file = null; + if (isset($files[0]['file'])) { + $file = $files[0]; + } elseif (isset($files[1]['file'])) { + $file = $files[1]; + } + if ($file) { + $code = static::excerpt($file['file'], $file['line'], 1); + } + $trace = static::trace(['start' => $data['start'], 'depth' => '20']); + $insertOpts = ['before' => '{:', 'after' => '}']; + $context = []; + $links = []; + $info = ''; + + foreach ((array)$data['context'] as $var => $value) { + $context[] = "\${$var} = " . static::exportVar($value, 3); + } + + switch ($this->_outputFormat) { + case false: + $this->_data[] = compact('context', 'trace') + $data; + + return; + case 'log': + static::log(compact('context', 'trace') + $data); + + return; + } + + $data['trace'] = $trace; + $data['id'] = 'cakeErr' . uniqid(); + $tpl = $this->_templates[$this->_outputFormat] + $this->_templates['base']; + + if (isset($tpl['links'])) { + foreach ($tpl['links'] as $key => $val) { + $links[$key] = Text::insert($val, $data, $insertOpts); + } + } + + if (!empty($tpl['escapeContext'])) { + $context = h($context); + $data['description'] = h($data['description']); + } + + $infoData = compact('code', 'context', 'trace'); + foreach ($infoData as $key => $value) { + if (empty($value) || !isset($tpl[$key])) { + continue; + } + if (is_array($value)) { + $value = implode("\n", $value); + } + $info .= Text::insert($tpl[$key], [$key => $value] + $data, $insertOpts); + } + $links = implode(' ', $links); + + if (isset($tpl['callback']) && is_callable($tpl['callback'])) { + call_user_func($tpl['callback'], $data, compact('links', 'info')); + + return; + } + echo Text::insert($tpl['error'], compact('links', 'info') + $data, $insertOpts); + } + + /** + * Get the type of the given variable. Will return the class name + * for objects. + * + * @param mixed $var The variable to get the type of. + * @return string The type of variable. + */ + public static function getType($var) + { + if (is_object($var)) { + return get_class($var); + } + if ($var === null) { + return 'null'; + } + if (is_string($var)) { + return 'string'; + } + if (is_array($var)) { + return 'array'; + } + if (is_int($var)) { + return 'integer'; + } + if (is_bool($var)) { + return 'boolean'; + } + if (is_float($var)) { + return 'float'; + } + if (is_resource($var)) { + return 'resource'; + } + + return 'unknown'; + } + + /** + * Prints out debug information about given variable. + * + * @param mixed $var Variable to show debug information for. + * @param array $location If contains keys "file" and "line" their values will + * be used to show location info. + * @param bool|null $showHtml If set to true, the method prints the debug + * data in a browser-friendly way. + * @return void + */ + public static function printVar($var, $location = [], $showHtml = null) + { + $location += ['file' => null, 'line' => null]; + $file = $location['file']; + $line = $location['line']; + $lineInfo = ''; + if ($file) { + $search = []; + if (defined('ROOT')) { + $search = [ROOT]; + } + if (defined('CAKE_CORE_INCLUDE_PATH')) { + array_unshift($search, CAKE_CORE_INCLUDE_PATH); + } + $file = str_replace($search, '', $file); + } + $html = << +%s +
+%s
+
+ +HTML; + $text = <<%s (line %s)', $file, $line); + } + } + printf($template, $lineInfo, $var); + } + + /** + * Verifies that the application's salt and cipher seed value has been changed from the default value. + * + * @return void + */ + public static function checkSecurityKeys() + { + if (Security::getSalt() === '__SALT__') { + trigger_error(sprintf('Please change the value of %s in %s to a salt value specific to your application.', '\'Security.salt\'', 'ROOT/config/app.php'), E_USER_NOTICE); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Error/ErrorHandler.php b/app/vendor/cakephp/cakephp/src/Error/ErrorHandler.php new file mode 100644 index 000000000..633b35f05 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Error/ErrorHandler.php @@ -0,0 +1,200 @@ + 1. + * + * ### Uncaught exceptions + * + * When debug < 1 a CakeException will render 404 or 500 errors. If an uncaught exception is thrown + * and it is a type that ErrorHandler does not know about it will be treated as a 500 error. + * + * ### Implementing application specific exception handling + * + * You can implement application specific exception handling in one of a few ways. Each approach + * gives you different amounts of control over the exception handling process. + * + * - Modify config/error.php and setup custom exception handling. + * - Use the `exceptionRenderer` option to inject an Exception renderer. This will + * let you keep the existing handling logic but override the rendering logic. + * + * #### Create your own Exception handler + * + * This gives you full control over the exception handling process. The class you choose should be + * loaded in your config/error.php and registered as the default exception handler. + * + * #### Using a custom renderer with `exceptionRenderer` + * + * If you don't want to take control of the exception handling, but want to change how exceptions are + * rendered you can use `exceptionRenderer` option to choose a class to render exception pages. By default + * `Cake\Error\ExceptionRenderer` is used. Your custom exception renderer class should be placed in src/Error. + * + * Your custom renderer should expect an exception in its constructor, and implement a render method. + * Failing to do so will cause additional errors. + * + * #### Logging exceptions + * + * Using the built-in exception handling, you can log all the exceptions + * that are dealt with by ErrorHandler by setting `log` option to true in your config/error.php. + * Enabling this will log every exception to Log and the configured loggers. + * + * ### PHP errors + * + * Error handler also provides the built in features for handling php errors (trigger_error). + * While in debug mode, errors will be output to the screen using debugger. While in production mode, + * errors will be logged to Log. You can control which errors are logged by setting + * `errorLevel` option in config/error.php. + * + * #### Logging errors + * + * When ErrorHandler is used for handling errors, you can enable error logging by setting the `log` + * option to true. This will log all errors to the configured log handlers. + * + * #### Controlling what errors are logged/displayed + * + * You can control which errors are logged / displayed by ErrorHandler by setting `errorLevel`. Setting this + * to one or a combination of a few of the E_* constants will only enable the specified errors: + * + * ``` + * $options['errorLevel'] = E_ALL & ~E_NOTICE; + * ``` + * + * Would enable handling for all non Notice errors. + * + * @see \Cake\Error\ExceptionRenderer for more information on how to customize exception rendering. + */ +class ErrorHandler extends BaseErrorHandler +{ + /** + * Constructor + * + * @param array $options The options for error handling. + */ + public function __construct($options = []) + { + $defaults = [ + 'log' => true, + 'trace' => false, + 'exceptionRenderer' => ExceptionRenderer::class, + ]; + $this->_options = $options + $defaults; + } + + /** + * Display an error. + * + * Template method of BaseErrorHandler. + * + * @param array $error An array of error data. + * @param bool $debug Whether or not the app is in debug mode. + * @return void + */ + protected function _displayError($error, $debug) + { + if (!$debug) { + return; + } + Debugger::getInstance()->outputError($error); + } + + /** + * Displays an exception response body. + * + * @param \Exception $exception The exception to display. + * @return void + * @throws \Exception When the chosen exception renderer is invalid. + */ + protected function _displayException($exception) + { + $rendererClassName = App::className($this->_options['exceptionRenderer'], 'Error'); + try { + if (!$rendererClassName) { + throw new Exception("$rendererClassName is an invalid class."); + } + /** @var \Cake\Error\ExceptionRendererInterface $renderer */ + $renderer = new $rendererClassName($exception); + $response = $renderer->render(); + $this->_clearOutput(); + $this->_sendResponse($response); + } catch (Throwable $exception) { + $this->_logInternalError($exception); + } catch (Exception $exception) { + $this->_logInternalError($exception); + } + } + + /** + * Clear output buffers so error pages display properly. + * + * Easily stubbed in testing. + * + * @return void + */ + protected function _clearOutput() + { + while (ob_get_level()) { + ob_end_clean(); + } + } + + /** + * Logs both PHP5 and PHP7 errors. + * + * The PHP5 part will be removed with 4.0. + * + * @param \Throwable|\Exception $exception Exception. + * + * @return void + */ + protected function _logInternalError($exception) + { + // Disable trace for internal errors. + $this->_options['trace'] = false; + $message = sprintf( + "[%s] %s\n%s", // Keeping same message format + get_class($exception), + $exception->getMessage(), + $exception->getTraceAsString() + ); + trigger_error($message, E_USER_ERROR); + } + + /** + * Method that can be easily stubbed in testing. + * + * @param string|\Cake\Http\Response $response Either the message or response object. + * @return void + */ + protected function _sendResponse($response) + { + if (is_string($response)) { + echo $response; + + return; + } + + $emitter = new ResponseEmitter(); + $emitter->emit($response); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Error/ExceptionRenderer.php b/app/vendor/cakephp/cakephp/src/Error/ExceptionRenderer.php new file mode 100644 index 000000000..1d45150e3 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Error/ExceptionRenderer.php @@ -0,0 +1,398 @@ +error = $exception; + $this->controller = $this->_getController(); + } + + /** + * Returns the unwrapped exception object in case we are dealing with + * a PHP 7 Error object + * + * @param \Exception $exception The object to unwrap + * @return \Exception|\Error + */ + protected function _unwrap($exception) + { + return $exception instanceof PHP7ErrorException ? $exception->getError() : $exception; + } + + /** + * Get the controller instance to handle the exception. + * Override this method in subclasses to customize the controller used. + * This method returns the built in `ErrorController` normally, or if an error is repeated + * a bare controller will be used. + * + * @return \Cake\Controller\Controller + * @triggers Controller.startup $controller + */ + protected function _getController() + { + if (!$request = Router::getRequest(true)) { + $request = ServerRequestFactory::fromGlobals(); + } + $response = new Response(); + $controller = null; + + try { + $class = App::className('Error', 'Controller', 'Controller'); + /* @var \Cake\Controller\Controller $controller */ + $controller = new $class($request, $response); + $controller->startupProcess(); + $startup = true; + } catch (Exception $e) { + $startup = false; + } + + // Retry RequestHandler, as another aspect of startupProcess() + // could have failed. Ignore any exceptions out of startup, as + // there could be userland input data parsers. + if ($startup === false && !empty($controller) && isset($controller->RequestHandler)) { + try { + $event = new Event('Controller.startup', $controller); + $controller->RequestHandler->startup($event); + } catch (Exception $e) { + } + } + if (empty($controller)) { + $controller = new Controller($request, $response); + } + + return $controller; + } + + /** + * Renders the response for the exception. + * + * @return \Cake\Http\Response The response to be sent. + */ + public function render() + { + $exception = $this->error; + $code = $this->_code($exception); + $method = $this->_method($exception); + $template = $this->_template($exception, $method, $code); + $unwrapped = $this->_unwrap($exception); + + $isDebug = Configure::read('debug'); + if (($isDebug || $exception instanceof HttpException) && + method_exists($this, $method) + ) { + return $this->_customMethod($method, $unwrapped); + } + + $message = $this->_message($exception, $code); + $url = $this->controller->getRequest()->getRequestTarget(); + $response = $this->controller->getResponse(); + + if ($exception instanceof CakeException) { + foreach ((array)$exception->responseHeader() as $key => $value) { + $response = $response->withHeader($key, $value); + } + } + $response = $response->withStatus($code); + + $viewVars = [ + 'message' => $message, + 'url' => h($url), + 'error' => $unwrapped, + 'code' => $code, + '_serialize' => ['message', 'url', 'code'] + ]; + if ($isDebug) { + $viewVars['trace'] = Debugger::formatTrace($unwrapped->getTrace(), [ + 'format' => 'array', + 'args' => false + ]); + $viewVars['file'] = $exception->getFile() ?: 'null'; + $viewVars['line'] = $exception->getLine() ?: 'null'; + $viewVars['_serialize'][] = 'file'; + $viewVars['_serialize'][] = 'line'; + } + $this->controller->set($viewVars); + + if ($unwrapped instanceof CakeException && $isDebug) { + $this->controller->set($unwrapped->getAttributes()); + } + $this->controller->response = $response; + + return $this->_outputMessage($template); + } + + /** + * Render a custom error method/template. + * + * @param string $method The method name to invoke. + * @param \Exception $exception The exception to render. + * @return \Cake\Http\Response The response to send. + */ + protected function _customMethod($method, $exception) + { + $result = call_user_func([$this, $method], $exception); + $this->_shutdown(); + if (is_string($result)) { + $result = $this->controller->response->withStringBody($result); + } + + return $result; + } + + /** + * Get method name + * + * @param \Exception $exception Exception instance. + * @return string + */ + protected function _method(Exception $exception) + { + $exception = $this->_unwrap($exception); + list(, $baseClass) = namespaceSplit(get_class($exception)); + + if (substr($baseClass, -9) === 'Exception') { + $baseClass = substr($baseClass, 0, -9); + } + + $method = Inflector::variable($baseClass) ?: 'error500'; + + return $this->method = $method; + } + + /** + * Get error message. + * + * @param \Exception $exception Exception. + * @param int $code Error code. + * @return string Error message + */ + protected function _message(Exception $exception, $code) + { + $exception = $this->_unwrap($exception); + $message = $exception->getMessage(); + + if (!Configure::read('debug') && + !($exception instanceof HttpException) + ) { + if ($code < 500) { + $message = __d('cake', 'Not Found'); + } else { + $message = __d('cake', 'An Internal Error Has Occurred.'); + } + } + + return $message; + } + + /** + * Get template for rendering exception info. + * + * @param \Exception $exception Exception instance. + * @param string $method Method name. + * @param int $code Error code. + * @return string Template name + */ + protected function _template(Exception $exception, $method, $code) + { + $exception = $this->_unwrap($exception); + $isHttpException = $exception instanceof HttpException; + + if (!Configure::read('debug') && !$isHttpException || $isHttpException) { + $template = 'error500'; + if ($code < 500) { + $template = 'error400'; + } + + return $this->template = $template; + } + + $template = $method ?: 'error500'; + + if ($exception instanceof PDOException) { + $template = 'pdo_error'; + } + + return $this->template = $template; + } + + /** + * Get an error code value within range 400 to 506 + * + * @param \Exception $exception Exception. + * @return int Error code value within range 400 to 506 + */ + protected function _code(Exception $exception) + { + $code = 500; + $exception = $this->_unwrap($exception); + $errorCode = $exception->getCode(); + if ($errorCode >= 400 && $errorCode < 506) { + $code = $errorCode; + } + + return $code; + } + + /** + * Generate the response using the controller object. + * + * @param string $template The template to render. + * @return \Cake\Http\Response A response object that can be sent. + */ + protected function _outputMessage($template) + { + try { + $this->controller->render($template); + + return $this->_shutdown(); + } catch (MissingTemplateException $e) { + $attributes = $e->getAttributes(); + if (isset($attributes['file']) && strpos($attributes['file'], 'error500') !== false) { + return $this->_outputMessageSafe('error500'); + } + + return $this->_outputMessage('error500'); + } catch (MissingPluginException $e) { + $attributes = $e->getAttributes(); + if (isset($attributes['plugin']) && $attributes['plugin'] === $this->controller->getPlugin()) { + $this->controller->setPlugin(null); + } + + return $this->_outputMessageSafe('error500'); + } catch (Exception $e) { + return $this->_outputMessageSafe('error500'); + } + } + + /** + * A safer way to render error messages, replaces all helpers, with basics + * and doesn't call component methods. + * + * @param string $template The template to render. + * @return \Cake\Http\Response A response object that can be sent. + */ + protected function _outputMessageSafe($template) + { + $helpers = ['Form', 'Html']; + $this->controller->helpers = $helpers; + $builder = $this->controller->viewBuilder(); + $builder->setHelpers($helpers, false) + ->setLayoutPath('') + ->setTemplatePath('Error'); + $view = $this->controller->createView('View'); + + $this->controller->response = $this->controller->response + ->withType('html') + ->withStringBody($view->render($template, 'error')); + + return $this->controller->response; + } + + /** + * Run the shutdown events. + * + * Triggers the afterFilter and afterDispatch events. + * + * @return \Cake\Http\Response The response to serve. + */ + protected function _shutdown() + { + $this->controller->dispatchEvent('Controller.shutdown'); + $dispatcher = DispatcherFactory::create(); + $eventManager = $dispatcher->getEventManager(); + foreach ($dispatcher->filters() as $filter) { + $eventManager->on($filter); + } + $args = [ + 'request' => $this->controller->request, + 'response' => $this->controller->response + ]; + $result = $dispatcher->dispatchEvent('Dispatcher.afterDispatch', $args); + + return $result->getData('response'); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Error/ExceptionRendererInterface.php b/app/vendor/cakephp/cakephp/src/Error/ExceptionRendererInterface.php new file mode 100644 index 000000000..f76939ca6 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Error/ExceptionRendererInterface.php @@ -0,0 +1,28 @@ +file = $file; + } + if ($line) { + $this->line = $line; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Error/Middleware/ErrorHandlerMiddleware.php b/app/vendor/cakephp/cakephp/src/Error/Middleware/ErrorHandlerMiddleware.php new file mode 100644 index 000000000..e433a6fb3 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Error/Middleware/ErrorHandlerMiddleware.php @@ -0,0 +1,236 @@ + ['Cake\Error\NotFoundException', 'Cake\Error\UnauthorizedException'] + * ``` + * + * - `trace` Should error logs include stack traces? + * + * @var array + */ + protected $_defaultConfig = [ + 'skipLog' => [], + 'log' => true, + 'trace' => false, + ]; + + /** + * Exception render. + * + * @var \Cake\Error\ExceptionRendererInterface|callable|string|null + */ + protected $exceptionRenderer; + + /** + * Constructor + * + * @param string|callable|null $exceptionRenderer The renderer or class name + * to use or a callable factory. If null, Configure::read('Error.exceptionRenderer') + * will be used. + * @param array $config Configuration options to use. If empty, `Configure::read('Error')` + * will be used. + */ + public function __construct($exceptionRenderer = null, array $config = []) + { + if ($exceptionRenderer) { + $this->exceptionRenderer = $exceptionRenderer; + } + + $config = $config ?: Configure::read('Error'); + $this->setConfig($config); + } + + /** + * Wrap the remaining middleware with error handling. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param \Psr\Http\Message\ResponseInterface $response The response. + * @param callable $next Callback to invoke the next middleware. + * @return \Psr\Http\Message\ResponseInterface A response + */ + public function __invoke($request, $response, $next) + { + try { + return $next($request, $response); + } catch (Throwable $exception) { + return $this->handleException($exception, $request, $response); + } catch (Exception $exception) { + return $this->handleException($exception, $request, $response); + } + } + + /** + * Handle an exception and generate an error response + * + * @param \Exception $exception The exception to handle. + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param \Psr\Http\Message\ResponseInterface $response The response. + * @return \Psr\Http\Message\ResponseInterface A response + */ + public function handleException($exception, $request, $response) + { + $renderer = $this->getRenderer($exception); + try { + $res = $renderer->render(); + $this->logException($request, $exception); + + return $res; + } catch (Throwable $exception) { + $this->logException($request, $exception); + $response = $this->handleInternalError($response); + } catch (Exception $exception) { + $this->logException($request, $exception); + $response = $this->handleInternalError($response); + } + + return $response; + } + + /** + * @param \Psr\Http\Message\ResponseInterface $response The response + * + * @return \Psr\Http\Message\ResponseInterface A response + */ + protected function handleInternalError($response) + { + $body = $response->getBody(); + $body->write('An Internal Server Error Occurred'); + + return $response->withStatus(500) + ->withBody($body); + } + + /** + * Get a renderer instance + * + * @param \Exception $exception The exception being rendered. + * @return \Cake\Error\ExceptionRendererInterface The exception renderer. + * @throws \Exception When the renderer class cannot be found. + */ + protected function getRenderer($exception) + { + if (!$this->exceptionRenderer) { + $this->exceptionRenderer = $this->getConfig('exceptionRenderer') ?: ExceptionRenderer::class; + } + + // For PHP5 backwards compatibility + if ($exception instanceof Error) { + $exception = new PHP7ErrorException($exception); + } + + if (is_string($this->exceptionRenderer)) { + $class = App::className($this->exceptionRenderer, 'Error'); + if (!$class) { + throw new Exception(sprintf( + "The '%s' renderer class could not be found.", + $this->exceptionRenderer + )); + } + + return new $class($exception); + } + $factory = $this->exceptionRenderer; + + return $factory($exception); + } + + /** + * Log an error for the exception if applicable. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The current request. + * @param \Exception $exception The exception to log a message for. + * @return void + */ + protected function logException($request, $exception) + { + if (!$this->getConfig('log')) { + return; + } + + foreach ((array)$this->getConfig('skipLog') as $class) { + if ($exception instanceof $class) { + return; + } + } + + Log::error($this->getMessage($request, $exception)); + } + + /** + * Generate the error log message. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The current request. + * @param \Exception $exception The exception to log a message for. + * @return string Error message + */ + protected function getMessage($request, $exception) + { + $message = sprintf( + '[%s] %s', + get_class($exception), + $exception->getMessage() + ); + $debug = Configure::read('debug'); + + if ($debug && $exception instanceof CakeException) { + $attributes = $exception->getAttributes(); + if ($attributes) { + $message .= "\nException Attributes: " . var_export($exception->getAttributes(), true); + } + } + $message .= "\nRequest URL: " . $request->getRequestTarget(); + $referer = $request->getHeaderLine('Referer'); + if ($referer) { + $message .= "\nReferer URL: " . $referer; + } + if ($this->getConfig('trace')) { + $message .= "\nStack Trace:\n" . $exception->getTraceAsString() . "\n\n"; + } + + return $message; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Error/PHP7ErrorException.php b/app/vendor/cakephp/cakephp/src/Error/PHP7ErrorException.php new file mode 100644 index 000000000..165b32bc6 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Error/PHP7ErrorException.php @@ -0,0 +1,63 @@ +_error = $error; + $this->message = $error->getMessage(); + $this->code = $error->getCode(); + $this->file = $error->getFile(); + $this->line = $error->getLine(); + $msg = sprintf( + '(%s) - %s in %s on %s', + get_class($error), + $this->message, + $this->file ?: 'null', + $this->line ?: 'null' + ); + parent::__construct($msg, $this->code, $error->getPrevious()); + } + + /** + * Returns the wrapped error object + * + * @return \Error + */ + public function getError() + { + return $this->_error; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Event/Decorator/AbstractDecorator.php b/app/vendor/cakephp/cakephp/src/Event/Decorator/AbstractDecorator.php new file mode 100644 index 000000000..e0cb24108 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Event/Decorator/AbstractDecorator.php @@ -0,0 +1,72 @@ +_callable = $callable; + $this->_options = $options; + } + + /** + * Invoke + * + * @link https://secure.php.net/manual/en/language.oop5.magic.php#object.invoke + * @return mixed + */ + public function __invoke() + { + return $this->_call(func_get_args()); + } + + /** + * Calls the decorated callable with the passed arguments. + * + * @param array $args Arguments for the callable. + * @return mixed + */ + protected function _call($args) + { + $callable = $this->_callable; + + return $callable(...$args); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Event/Decorator/ConditionDecorator.php b/app/vendor/cakephp/cakephp/src/Event/Decorator/ConditionDecorator.php new file mode 100644 index 000000000..7db1afb57 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Event/Decorator/ConditionDecorator.php @@ -0,0 +1,74 @@ +canTrigger($args[0])) { + return; + } + + return $this->_call($args); + } + + /** + * Checks if the event is triggered for this listener. + * + * @param \Cake\Event\Event $event Event object. + * @return bool + */ + public function canTrigger(Event $event) + { + $if = $this->_evaluateCondition('if', $event); + $unless = $this->_evaluateCondition('unless', $event); + + return $if && !$unless; + } + + /** + * Evaluates the filter conditions + * + * @param string $condition Condition type + * @param \Cake\Event\Event $event Event object + * @return bool + */ + protected function _evaluateCondition($condition, Event $event) + { + if (!isset($this->_options[$condition])) { + return $condition !== 'unless'; + } + if (!is_callable($this->_options[$condition])) { + throw new RuntimeException(self::class . ' the `' . $condition . '` condition is not a callable!'); + } + + return $this->_options[$condition]($event); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Event/Decorator/SubjectFilterDecorator.php b/app/vendor/cakephp/cakephp/src/Event/Decorator/SubjectFilterDecorator.php new file mode 100644 index 000000000..eb32f826c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Event/Decorator/SubjectFilterDecorator.php @@ -0,0 +1,63 @@ +canTrigger($args[0])) { + return false; + } + + return $this->_call($args); + } + + /** + * Checks if the event is triggered for this listener. + * + * @param \Cake\Event\Event $event Event object. + * @return bool + */ + public function canTrigger(Event $event) + { + $class = get_class($event->getSubject()); + if (!isset($this->_options['allowedSubject'])) { + throw new RuntimeException(self::class . ' Missing subject filter options!'); + } + if (is_string($this->_options['allowedSubject'])) { + $this->_options['allowedSubject'] = [$this->_options['allowedSubject']]; + } + + return in_array($class, $this->_options['allowedSubject']); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Event/Event.php b/app/vendor/cakephp/cakephp/src/Event/Event.php new file mode 100644 index 000000000..9c599a245 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Event/Event.php @@ -0,0 +1,283 @@ + $userData]); + * $event = new Event('User.afterRegister', $UserModel); + * ``` + * + * @param string $name Name of the event + * @param object|null $subject the object that this event applies to (usually the object that is generating the event) + * @param array|\ArrayAccess|null $data any value you wish to be transported with this event to it can be read by listeners + */ + public function __construct($name, $subject = null, $data = null) + { + $this->_name = $name; + $this->_subject = $subject; + $this->_data = (array)$data; + } + + /** + * Provides read-only access for the name and subject properties. + * + * @param string $attribute Attribute name. + * @return mixed + * @deprecated 3.4.0 Public properties will be removed. + */ + public function __get($attribute) + { + if (!in_array($attribute, ['name', 'subject', 'data', 'result'])) { + return $this->{$attribute}; + } + + $method = 'get' . ucfirst($attribute); + deprecationWarning( + "Event::\${$attribute} is deprecated. " . + "Use Event::{$method}() instead." + ); + if ($attribute === 'name' || $attribute === 'subject') { + return $this->{$attribute}(); + } + if ($attribute === 'data') { + return $this->_data; + } + if ($attribute === 'result') { + return $this->result; + } + } + + /** + * Provides backward compatibility for write access to data and result properties. + * + * @param string $attribute Attribute name. + * @param mixed $value The value to set. + * @return void + * @deprecated 3.4.0 Public properties will be removed. + */ + public function __set($attribute, $value) + { + $method = 'set' . ucfirst($attribute); + deprecationWarning( + "Event::\${$attribute} is deprecated. " . + "Use Event::{$method}() instead." + ); + if ($attribute === 'data') { + $this->_data = (array)$value; + } + if ($attribute === 'result') { + $this->result = $value; + } + } + + /** + * Returns the name of this event. This is usually used as the event identifier + * + * @return string + * @deprecated 3.4.0 use getName() instead. + */ + public function name() + { + deprecationWarning('Event::name() is deprecated. Use Event::getName() instead.'); + + return $this->_name; + } + + /** + * Returns the name of this event. This is usually used as the event identifier + * + * @return string + */ + public function getName() + { + return $this->_name; + } + + /** + * Returns the subject of this event + * + * @return object + * @deprecated 3.4.0 use getSubject() instead. + */ + public function subject() + { + deprecationWarning('Event::subject() is deprecated. Use Event::getSubject() instead.'); + + return $this->_subject; + } + + /** + * Returns the subject of this event + * + * @return object + */ + public function getSubject() + { + return $this->_subject; + } + + /** + * Stops the event from being used anymore + * + * @return void + */ + public function stopPropagation() + { + $this->_stopped = true; + } + + /** + * Check if the event is stopped + * + * @return bool True if the event is stopped + */ + public function isStopped() + { + return $this->_stopped; + } + + /** + * The result value of the event listeners + * + * @return mixed + * @deprecated 3.4.0 use getResult() instead. + */ + public function result() + { + deprecationWarning('Event::result() is deprecated. Use Event::getResult() instead.'); + + return $this->result; + } + + /** + * The result value of the event listeners + * + * @return mixed + */ + public function getResult() + { + return $this->result; + } + + /** + * Listeners can attach a result value to the event. + * + * @param mixed $value The value to set. + * @return $this + */ + public function setResult($value = null) + { + $this->result = $value; + + return $this; + } + + /** + * Access the event data/payload. + * + * @param string|null $key The data payload element to return, or null to return all data. + * @return array|mixed|null The data payload if $key is null, or the data value for the given $key. If the $key does not + * exist a null value is returned. + * @deprecated 3.4.0 use getData() instead. + */ + public function data($key = null) + { + deprecationWarning('Event::data() is deprecated. Use Event::getData() instead.'); + + return $this->getData($key); + } + + /** + * Access the event data/payload. + * + * @param string|null $key The data payload element to return, or null to return all data. + * @return array|mixed|null The data payload if $key is null, or the data value for the given $key. If the $key does not + * exist a null value is returned. + */ + public function getData($key = null) + { + if ($key !== null) { + return isset($this->_data[$key]) ? $this->_data[$key] : null; + } + + return (array)$this->_data; + } + + /** + * Assigns a value to the data/payload of this event. + * + * @param array|string $key An array will replace all payload data, and a key will set just that array item. + * @param mixed $value The value to set. + * @return $this + */ + public function setData($key, $value = null) + { + if (is_array($key)) { + $this->_data = $key; + } else { + $this->_data[$key] = $value; + } + + return $this; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Event/EventDispatcherInterface.php b/app/vendor/cakephp/cakephp/src/Event/EventDispatcherInterface.php new file mode 100644 index 000000000..9119ad1e1 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Event/EventDispatcherInterface.php @@ -0,0 +1,57 @@ +setEventManager($eventManager); + } + + return $this->getEventManager(); + } + + /** + * Returns the Cake\Event\EventManager manager instance for this object. + * + * You can use this instance to register any new listeners or callbacks to the + * object events, or create your own events and trigger them at will. + * + * @return \Cake\Event\EventManager + */ + public function getEventManager() + { + if ($this->_eventManager === null) { + $this->_eventManager = new EventManager(); + } + + return $this->_eventManager; + } + + /** + * Returns the Cake\Event\EventManager manager instance for this object. + * + * You can use this instance to register any new listeners or callbacks to the + * object events, or create your own events and trigger them at will. + * + * @param \Cake\Event\EventManager $eventManager the eventManager to set + * @return $this + */ + public function setEventManager(EventManager $eventManager) + { + $this->_eventManager = $eventManager; + + return $this; + } + + /** + * Wrapper for creating and dispatching events. + * + * Returns a dispatched event. + * + * @param string $name Name of the event. + * @param array|null $data Any value you wish to be transported with this event to + * it can be read by listeners. + * @param object|null $subject The object that this event applies to + * ($this by default). + * + * @return \Cake\Event\Event + */ + public function dispatchEvent($name, $data = null, $subject = null) + { + if ($subject === null) { + $subject = $this; + } + + $event = new $this->_eventClass($name, $subject, $data); + $this->getEventManager()->dispatch($event); + + return $event; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Event/EventInterface.php b/app/vendor/cakephp/cakephp/src/Event/EventInterface.php new file mode 100644 index 000000000..4733ccea6 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Event/EventInterface.php @@ -0,0 +1,89 @@ +_events = []; + } + + /** + * Adds an event to the list when event listing is enabled. + * + * @param \Cake\Event\Event $event An event to the list of dispatched events. + * @return void + */ + public function add(Event $event) + { + $this->_events[] = $event; + } + + /** + * Whether a offset exists + * + * @link https://secure.php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset An offset to check for. + * @return bool True on success or false on failure. + */ + public function offsetExists($offset) + { + return isset($this->_events[$offset]); + } + + /** + * Offset to retrieve + * + * @link https://secure.php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset The offset to retrieve. + * @return mixed Can return all value types. + */ + public function offsetGet($offset) + { + if ($this->offsetExists($offset)) { + return $this->_events[$offset]; + } + + return null; + } + + /** + * Offset to set + * + * @link https://secure.php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset The offset to assign the value to. + * @param mixed $value The value to set. + * @return void + */ + public function offsetSet($offset, $value) + { + $this->_events[$offset] = $value; + } + + /** + * Offset to unset + * + * @link https://secure.php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset The offset to unset. + * @return void + */ + public function offsetUnset($offset) + { + unset($this->_events[$offset]); + } + + /** + * Count elements of an object + * + * @link https://secure.php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + */ + public function count() + { + return count($this->_events); + } + + /** + * Checks if an event is in the list. + * + * @param string $name Event name. + * @return bool + */ + public function hasEvent($name) + { + foreach ($this->_events as $event) { + if ($event->getName() === $name) { + return true; + } + } + + return false; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Event/EventListenerInterface.php b/app/vendor/cakephp/cakephp/src/Event/EventListenerInterface.php new file mode 100644 index 000000000..5f7af679a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Event/EventListenerInterface.php @@ -0,0 +1,45 @@ + 'sendEmail', + * 'Article.afterBuy' => 'decrementInventory', + * 'User.onRegister' => ['callable' => 'logRegistration', 'priority' => 20, 'passParams' => true] + * ]; + * } + * ``` + * + * @return array associative array or event key names pointing to the function + * that should be called in the object when the respective event is fired + */ + public function implementedEvents(); +} diff --git a/app/vendor/cakephp/cakephp/src/Event/EventManager.php b/app/vendor/cakephp/cakephp/src/Event/EventManager.php new file mode 100644 index 000000000..2e5cd7d18 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Event/EventManager.php @@ -0,0 +1,527 @@ +_isGlobal = true; + + return static::$_generalManager; + } + + /** + * Adds a new listener to an event. + * + * @param callable|\Cake\Event\EventListenerInterface $callable PHP valid callback type or instance of Cake\Event\EventListenerInterface to be called + * when the event named with $eventKey is triggered. If a Cake\Event\EventListenerInterface instance is passed, then the `implementedEvents` + * method will be called on the object to register the declared events individually as methods to be managed by this class. + * It is possible to define multiple event handlers per event name. + * + * @param string|null $eventKey The event unique identifier name with which the callback will be associated. If $callable + * is an instance of Cake\Event\EventListenerInterface this argument will be ignored + * + * @param array $options used to set the `priority` flag to the listener. In the future more options may be added. + * Priorities are treated as queues. Lower values are called before higher ones, and multiple attachments + * added to the same priority queue will be treated in the order of insertion. + * + * @return void + * @throws \InvalidArgumentException When event key is missing or callable is not an + * instance of Cake\Event\EventListenerInterface. + * @deprecated 3.0.0 Use on() instead. + */ + public function attach($callable, $eventKey = null, array $options = []) + { + deprecationWarning('EventManager::attach() is deprecated. Use EventManager::on() instead.'); + if ($eventKey === null) { + $this->on($callable); + + return; + } + if ($options) { + $this->on($eventKey, $options, $callable); + + return; + } + $this->on($eventKey, $callable); + } + + /** + * {@inheritDoc} + */ + public function on($eventKey = null, $options = [], $callable = null) + { + if ($eventKey instanceof EventListenerInterface) { + $this->_attachSubscriber($eventKey); + + return $this; + } + $argCount = func_num_args(); + if ($argCount === 2) { + $this->_listeners[$eventKey][static::$defaultPriority][] = [ + 'callable' => $options + ]; + + return $this; + } + if ($argCount === 3) { + $priority = isset($options['priority']) ? $options['priority'] : static::$defaultPriority; + $this->_listeners[$eventKey][$priority][] = [ + 'callable' => $callable + ]; + + return $this; + } + throw new InvalidArgumentException( + 'Invalid arguments for EventManager::on(). ' . + "Expected 1, 2 or 3 arguments. Got {$argCount} arguments." + ); + } + + /** + * Auxiliary function to attach all implemented callbacks of a Cake\Event\EventListenerInterface class instance + * as individual methods on this manager + * + * @param \Cake\Event\EventListenerInterface $subscriber Event listener. + * @return void + */ + protected function _attachSubscriber(EventListenerInterface $subscriber) + { + foreach ((array)$subscriber->implementedEvents() as $eventKey => $function) { + $options = []; + $method = $function; + if (is_array($function) && isset($function['callable'])) { + list($method, $options) = $this->_extractCallable($function, $subscriber); + } elseif (is_array($function) && is_numeric(key($function))) { + foreach ($function as $f) { + list($method, $options) = $this->_extractCallable($f, $subscriber); + $this->on($eventKey, $options, $method); + } + continue; + } + if (is_string($method)) { + $method = [$subscriber, $function]; + } + $this->on($eventKey, $options, $method); + } + } + + /** + * Auxiliary function to extract and return a PHP callback type out of the callable definition + * from the return value of the `implementedEvents` method on a Cake\Event\EventListenerInterface + * + * @param array $function the array taken from a handler definition for an event + * @param \Cake\Event\EventListenerInterface $object The handler object + * @return callable + */ + protected function _extractCallable($function, $object) + { + $method = $function['callable']; + $options = $function; + unset($options['callable']); + if (is_string($method)) { + $method = [$object, $method]; + } + + return [$method, $options]; + } + + /** + * Removes a listener from the active listeners. + * + * @param callable|\Cake\Event\EventListenerInterface $callable any valid PHP callback type or an instance of EventListenerInterface + * @param string|null $eventKey The event unique identifier name with which the callback has been associated + * @return void + * @deprecated 3.0.0 Use off() instead. + */ + public function detach($callable, $eventKey = null) + { + deprecationWarning('EventManager::detach() is deprecated. Use EventManager::off() instead.'); + if ($eventKey === null) { + $this->off($callable); + + return; + } + $this->off($eventKey, $callable); + } + + /** + * {@inheritDoc} + */ + public function off($eventKey, $callable = null) + { + if ($eventKey instanceof EventListenerInterface) { + $this->_detachSubscriber($eventKey); + + return $this; + } + if ($callable instanceof EventListenerInterface) { + $this->_detachSubscriber($callable, $eventKey); + + return $this; + } + if ($callable === null && is_string($eventKey)) { + unset($this->_listeners[$eventKey]); + + return $this; + } + if ($callable === null) { + foreach (array_keys($this->_listeners) as $name) { + $this->off($name, $eventKey); + } + + return $this; + } + if (empty($this->_listeners[$eventKey])) { + return $this; + } + foreach ($this->_listeners[$eventKey] as $priority => $callables) { + foreach ($callables as $k => $callback) { + if ($callback['callable'] === $callable) { + unset($this->_listeners[$eventKey][$priority][$k]); + break; + } + } + } + + return $this; + } + + /** + * Auxiliary function to help detach all listeners provided by an object implementing EventListenerInterface + * + * @param \Cake\Event\EventListenerInterface $subscriber the subscriber to be detached + * @param string|null $eventKey optional event key name to unsubscribe the listener from + * @return void + */ + protected function _detachSubscriber(EventListenerInterface $subscriber, $eventKey = null) + { + $events = (array)$subscriber->implementedEvents(); + if (!empty($eventKey) && empty($events[$eventKey])) { + return; + } + if (!empty($eventKey)) { + $events = [$eventKey => $events[$eventKey]]; + } + foreach ($events as $key => $function) { + if (is_array($function)) { + if (is_numeric(key($function))) { + foreach ($function as $handler) { + $handler = isset($handler['callable']) ? $handler['callable'] : $handler; + $this->off($key, [$subscriber, $handler]); + } + continue; + } + $function = $function['callable']; + } + $this->off($key, [$subscriber, $function]); + } + } + + /** + * {@inheritDoc} + */ + public function dispatch($event) + { + if (is_string($event)) { + $event = new Event($event); + } + + $listeners = $this->listeners($event->getName()); + + if ($this->_trackEvents) { + $this->addEventToList($event); + } + + if (!$this->_isGlobal && static::instance()->isTrackingEvents()) { + static::instance()->addEventToList($event); + } + + if (empty($listeners)) { + return $event; + } + + foreach ($listeners as $listener) { + if ($event->isStopped()) { + break; + } + $result = $this->_callListener($listener['callable'], $event); + if ($result === false) { + $event->stopPropagation(); + } + if ($result !== null) { + $event->setResult($result); + } + } + + return $event; + } + + /** + * Calls a listener. + * + * @param callable $listener The listener to trigger. + * @param \Cake\Event\Event $event Event instance. + * @return mixed The result of the $listener function. + */ + protected function _callListener(callable $listener, Event $event) + { + $data = $event->getData(); + + return $listener($event, ...array_values($data)); + } + + /** + * {@inheritDoc} + */ + public function listeners($eventKey) + { + $localListeners = []; + if (!$this->_isGlobal) { + $localListeners = $this->prioritisedListeners($eventKey); + $localListeners = empty($localListeners) ? [] : $localListeners; + } + $globalListeners = static::instance()->prioritisedListeners($eventKey); + $globalListeners = empty($globalListeners) ? [] : $globalListeners; + + $priorities = array_merge(array_keys($globalListeners), array_keys($localListeners)); + $priorities = array_unique($priorities); + asort($priorities); + + $result = []; + foreach ($priorities as $priority) { + if (isset($globalListeners[$priority])) { + $result = array_merge($result, $globalListeners[$priority]); + } + if (isset($localListeners[$priority])) { + $result = array_merge($result, $localListeners[$priority]); + } + } + + return $result; + } + + /** + * Returns the listeners for the specified event key indexed by priority + * + * @param string $eventKey Event key. + * @return array + */ + public function prioritisedListeners($eventKey) + { + if (empty($this->_listeners[$eventKey])) { + return []; + } + + return $this->_listeners[$eventKey]; + } + + /** + * Returns the listeners matching a specified pattern + * + * @param string $eventKeyPattern Pattern to match. + * @return array + */ + public function matchingListeners($eventKeyPattern) + { + $matchPattern = '/' . preg_quote($eventKeyPattern, '/') . '/'; + $matches = array_intersect_key( + $this->_listeners, + array_flip( + preg_grep($matchPattern, array_keys($this->_listeners), 0) + ) + ); + + return $matches; + } + + /** + * Returns the event list. + * + * @return \Cake\Event\EventList + */ + public function getEventList() + { + return $this->_eventList; + } + + /** + * Adds an event to the list if the event list object is present. + * + * @param \Cake\Event\Event $event An event to add to the list. + * @return $this + */ + public function addEventToList(Event $event) + { + if ($this->_eventList) { + $this->_eventList->add($event); + } + + return $this; + } + + /** + * Enables / disables event tracking at runtime. + * + * @param bool $enabled True or false to enable / disable it. + * @return $this + */ + public function trackEvents($enabled) + { + $this->_trackEvents = (bool)$enabled; + + return $this; + } + + /** + * Returns whether this manager is set up to track events + * + * @return bool + */ + public function isTrackingEvents() + { + return $this->_trackEvents && $this->_eventList; + } + + /** + * Enables the listing of dispatched events. + * + * @param \Cake\Event\EventList $eventList The event list object to use. + * @return $this + */ + public function setEventList(EventList $eventList) + { + $this->_eventList = $eventList; + $this->_trackEvents = true; + + return $this; + } + + /** + * Disables the listing of dispatched events. + * + * @return $this + */ + public function unsetEventList() + { + $this->_eventList = null; + $this->_trackEvents = false; + + return $this; + } + + /** + * Debug friendly object properties. + * + * @return array + */ + public function __debugInfo() + { + $properties = get_object_vars($this); + $properties['_generalManager'] = '(object) EventManager'; + $properties['_listeners'] = []; + foreach ($this->_listeners as $key => $priorities) { + $listenerCount = 0; + foreach ($priorities as $listeners) { + $listenerCount += count($listeners); + } + $properties['_listeners'][$key] = $listenerCount . ' listener(s)'; + } + if ($this->_eventList) { + $count = count($this->_eventList); + for ($i = 0; $i < $count; $i++) { + $event = $this->_eventList[$i]; + $subject = $event->getSubject(); + $properties['_dispatchedEvents'][] = $event->getName() . ' with ' . + (is_object($subject) ? 'subject ' . get_class($subject) : 'no subject'); + } + } else { + $properties['_dispatchedEvents'] = null; + } + unset($properties['_eventList']); + + return $properties; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Event/EventManagerInterface.php b/app/vendor/cakephp/cakephp/src/Event/EventManagerInterface.php new file mode 100644 index 000000000..a5979cd8c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Event/EventManagerInterface.php @@ -0,0 +1,112 @@ +on($listener); + * ``` + * + * Binding with no options: + * + * ``` + * $eventManager->on('Model.beforeSave', $callable); + * ``` + * + * Binding with options: + * + * ``` + * $eventManager->on('Model.beforeSave', ['priority' => 90], $callable); + * ``` + * + * @param string|\Cake\Event\EventListenerInterface|null $eventKey The event unique identifier name + * with which the callback will be associated. If $eventKey is an instance of + * Cake\Event\EventListenerInterface its events will be bound using the `implementedEvents` methods. + * + * @param array|callable $options Either an array of options or the callable you wish to + * bind to $eventKey. If an array of options, the `priority` key can be used to define the order. + * Priorities are treated as queues. Lower values are called before higher ones, and multiple attachments + * added to the same priority queue will be treated in the order of insertion. + * + * @param callable|null $callable The callable function you want invoked. + * + * @return $this + * @throws \InvalidArgumentException When event key is missing or callable is not an + * instance of Cake\Event\EventListenerInterface. + */ + public function on($eventKey = null, $options = [], $callable = null); + + /** + * Remove a listener from the active listeners. + * + * Remove a EventListenerInterface entirely: + * + * ``` + * $manager->off($listener); + * ``` + * + * Remove all listeners for a given event: + * + * ``` + * $manager->off('My.event'); + * ``` + * + * Remove a specific listener: + * + * ``` + * $manager->off('My.event', $callback); + * ``` + * + * Remove a callback from all events: + * + * ``` + * $manager->off($callback); + * ``` + * + * @param string|\Cake\Event\EventListenerInterface $eventKey The event unique identifier name + * with which the callback has been associated, or the $listener you want to remove. + * @param callable|null $callable The callback you want to detach. + * @return $this + */ + public function off($eventKey, $callable = null); + + /** + * Dispatches a new event to all configured listeners + * + * @param string|\Cake\Event\EventInterface $event The event key name or instance of EventInterface. + * @return \Cake\Event\EventInterface + * @triggers $event + */ + public function dispatch($event); + + /** + * Returns a list of all listeners for an eventKey in the order they should be called + * + * @param string $eventKey Event key. + * @return array + */ + public function listeners($eventKey); +} diff --git a/app/vendor/cakephp/cakephp/src/Event/EventManagerTrait.php b/app/vendor/cakephp/cakephp/src/Event/EventManagerTrait.php new file mode 100644 index 000000000..efc0a5abe --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Event/EventManagerTrait.php @@ -0,0 +1,26 @@ +doStuff(); + $event = new Event('Orders.afterPlace', $this, [ + 'order' => $order + ]); + $this->getEventManager()->dispatch($event); + } +} + +$orders = new Orders(); +$orders->getEventManager()->on(function ($event) { + // Do something after the order was placed + ... +}, 'Orders.afterPlace'); + +$orders->placeOrder($order); +``` + +The above code allows you to easily notify the other parts of the application that an order has been created. +You can then do tasks like send email notifications, update stock, log relevant statistics and other tasks +in separate objects that focus on those concerns. + +## Documentation + +Please make sure you check the [official documentation](https://book.cakephp.org/3.0/en/core-libraries/events.html) diff --git a/app/vendor/cakephp/cakephp/src/Event/composer.json b/app/vendor/cakephp/cakephp/src/Event/composer.json new file mode 100644 index 000000000..e984d41e2 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Event/composer.json @@ -0,0 +1,34 @@ +{ + "name": "cakephp/event", + "description": "CakePHP event dispatcher library that helps implementing the observer pattern", + "type": "library", + "keywords": [ + "cakephp", + "event", + "dispatcher", + "observer pattern" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/event/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/event" + }, + "require": { + "php": ">=5.6.0", + "cakephp/core": "^3.6.0" + }, + "autoload": { + "psr-4": { + "Cake\\Event\\": "." + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Filesystem/File.php b/app/vendor/cakephp/cakephp/src/Filesystem/File.php new file mode 100644 index 000000000..e0ed1a44c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Filesystem/File.php @@ -0,0 +1,660 @@ +Folder = new Folder($splInfo->getPath(), $create, $mode); + if (!is_dir($path)) { + $this->name = ltrim($splInfo->getFilename(), '/\\'); + } + $this->pwd(); + $create && !$this->exists() && $this->safe($path) && $this->create(); + } + + /** + * Closes the current file if it is opened + */ + public function __destruct() + { + $this->close(); + } + + /** + * Creates the file. + * + * @return bool Success + */ + public function create() + { + $dir = $this->Folder->pwd(); + + if (is_dir($dir) && is_writable($dir) && !$this->exists() && touch($this->path)) { + return true; + } + + return false; + } + + /** + * Opens the current file with a given $mode + * + * @param string $mode A valid 'fopen' mode string (r|w|a ...) + * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't + * @return bool True on success, false on failure + */ + public function open($mode = 'r', $force = false) + { + if (!$force && is_resource($this->handle)) { + return true; + } + if ($this->exists() === false && $this->create() === false) { + return false; + } + + $this->handle = fopen($this->path, $mode); + + return is_resource($this->handle); + } + + /** + * Return the contents of this file as a string. + * + * @param string|bool $bytes where to start + * @param string $mode A `fread` compatible mode. + * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't + * @return string|false string on success, false on failure + */ + public function read($bytes = false, $mode = 'rb', $force = false) + { + if ($bytes === false && $this->lock === null) { + return file_get_contents($this->path); + } + if ($this->open($mode, $force) === false) { + return false; + } + if ($this->lock !== null && flock($this->handle, LOCK_SH) === false) { + return false; + } + if (is_int($bytes)) { + return fread($this->handle, $bytes); + } + + $data = ''; + while (!feof($this->handle)) { + $data .= fgets($this->handle, 4096); + } + + if ($this->lock !== null) { + flock($this->handle, LOCK_UN); + } + if ($bytes === false) { + $this->close(); + } + + return trim($data); + } + + /** + * Sets or gets the offset for the currently opened file. + * + * @param int|bool $offset The $offset in bytes to seek. If set to false then the current offset is returned. + * @param int $seek PHP Constant SEEK_SET | SEEK_CUR | SEEK_END determining what the $offset is relative to + * @return int|bool True on success, false on failure (set mode), false on failure or integer offset on success (get mode) + */ + public function offset($offset = false, $seek = SEEK_SET) + { + if ($offset === false) { + if (is_resource($this->handle)) { + return ftell($this->handle); + } + } elseif ($this->open() === true) { + return fseek($this->handle, $offset, $seek) === 0; + } + + return false; + } + + /** + * Prepares an ASCII string for writing. Converts line endings to the + * correct terminator for the current platform. If Windows, "\r\n" will be used, + * all other platforms will use "\n" + * + * @param string $data Data to prepare for writing. + * @param bool $forceWindows If true forces Windows new line string. + * @return string The with converted line endings. + */ + public static function prepare($data, $forceWindows = false) + { + $lineBreak = "\n"; + if (DIRECTORY_SEPARATOR === '\\' || $forceWindows === true) { + $lineBreak = "\r\n"; + } + + return strtr($data, ["\r\n" => $lineBreak, "\n" => $lineBreak, "\r" => $lineBreak]); + } + + /** + * Write given data to this file. + * + * @param string $data Data to write to this File. + * @param string $mode Mode of writing. {@link https://secure.php.net/fwrite See fwrite()}. + * @param bool $force Force the file to open + * @return bool Success + */ + public function write($data, $mode = 'w', $force = false) + { + $success = false; + if ($this->open($mode, $force) === true) { + if ($this->lock !== null && flock($this->handle, LOCK_EX) === false) { + return false; + } + + if (fwrite($this->handle, $data) !== false) { + $success = true; + } + if ($this->lock !== null) { + flock($this->handle, LOCK_UN); + } + } + + return $success; + } + + /** + * Append given data string to this file. + * + * @param string $data Data to write + * @param bool $force Force the file to open + * @return bool Success + */ + public function append($data, $force = false) + { + return $this->write($data, 'a', $force); + } + + /** + * Closes the current file if it is opened. + * + * @return bool True if closing was successful or file was already closed, otherwise false + */ + public function close() + { + if (!is_resource($this->handle)) { + return true; + } + + return fclose($this->handle); + } + + /** + * Deletes the file. + * + * @return bool Success + */ + public function delete() + { + if (is_resource($this->handle)) { + fclose($this->handle); + $this->handle = null; + } + if ($this->exists()) { + return unlink($this->path); + } + + return false; + } + + /** + * Returns the file info as an array with the following keys: + * + * - dirname + * - basename + * - extension + * - filename + * - filesize + * - mime + * + * @return array File information. + */ + public function info() + { + if (!$this->info) { + $this->info = pathinfo($this->path); + } + if (!isset($this->info['filename'])) { + $this->info['filename'] = $this->name(); + } + if (!isset($this->info['filesize'])) { + $this->info['filesize'] = $this->size(); + } + if (!isset($this->info['mime'])) { + $this->info['mime'] = $this->mime(); + } + + return $this->info; + } + + /** + * Returns the file extension. + * + * @return string|false The file extension, false if extension cannot be extracted. + */ + public function ext() + { + if (!$this->info) { + $this->info(); + } + if (isset($this->info['extension'])) { + return $this->info['extension']; + } + + return false; + } + + /** + * Returns the file name without extension. + * + * @return string|false The file name without extension, false if name cannot be extracted. + */ + public function name() + { + if (!$this->info) { + $this->info(); + } + if (isset($this->info['extension'])) { + return static::_basename($this->name, '.' . $this->info['extension']); + } + if ($this->name) { + return $this->name; + } + + return false; + } + + /** + * Returns the file basename. simulate the php basename() for multibyte (mb_basename). + * + * @param string $path Path to file + * @param string|null $ext The name of the extension + * @return string the file basename. + */ + protected static function _basename($path, $ext = null) + { + // check for multibyte string and use basename() if not found + if (mb_strlen($path) === strlen($path)) { + return ($ext === null)? basename($path) : basename($path, $ext); + } + + $splInfo = new SplFileInfo($path); + $name = ltrim($splInfo->getFilename(), '/\\'); + + if ($ext === null || $ext === '') { + return $name; + } + $ext = preg_quote($ext); + $new = preg_replace("/({$ext})$/u", "", $name); + + // basename of '/etc/.d' is '.d' not '' + return ($new === '')? $name : $new; + } + + /** + * Makes file name safe for saving + * + * @param string|null $name The name of the file to make safe if different from $this->name + * @param string|null $ext The name of the extension to make safe if different from $this->ext + * @return string The extension of the file + */ + public function safe($name = null, $ext = null) + { + if (!$name) { + $name = $this->name; + } + if (!$ext) { + $ext = $this->ext(); + } + + return preg_replace("/(?:[^\w\.-]+)/", '_', static::_basename($name, $ext)); + } + + /** + * Get md5 Checksum of file with previous check of Filesize + * + * @param int|bool $maxsize in MB or true to force + * @return string|false md5 Checksum {@link https://secure.php.net/md5_file See md5_file()}, or false in case of an error + */ + public function md5($maxsize = 5) + { + if ($maxsize === true) { + return md5_file($this->path); + } + + $size = $this->size(); + if ($size && $size < ($maxsize * 1024) * 1024) { + return md5_file($this->path); + } + + return false; + } + + /** + * Returns the full path of the file. + * + * @return string Full path to the file + */ + public function pwd() + { + if ($this->path === null) { + $dir = $this->Folder->pwd(); + if (is_dir($dir)) { + $this->path = $this->Folder->slashTerm($dir) . $this->name; + } + } + + return $this->path; + } + + /** + * Returns true if the file exists. + * + * @return bool True if it exists, false otherwise + */ + public function exists() + { + $this->clearStatCache(); + + return (file_exists($this->path) && is_file($this->path)); + } + + /** + * Returns the "chmod" (permissions) of the file. + * + * @return string|false Permissions for the file, or false in case of an error + */ + public function perms() + { + if ($this->exists()) { + return substr(sprintf('%o', fileperms($this->path)), -4); + } + + return false; + } + + /** + * Returns the file size + * + * @return int|false Size of the file in bytes, or false in case of an error + */ + public function size() + { + if ($this->exists()) { + return filesize($this->path); + } + + return false; + } + + /** + * Returns true if the file is writable. + * + * @return bool True if it's writable, false otherwise + */ + public function writable() + { + return is_writable($this->path); + } + + /** + * Returns true if the File is executable. + * + * @return bool True if it's executable, false otherwise + */ + public function executable() + { + return is_executable($this->path); + } + + /** + * Returns true if the file is readable. + * + * @return bool True if file is readable, false otherwise + */ + public function readable() + { + return is_readable($this->path); + } + + /** + * Returns the file's owner. + * + * @return int|false The file owner, or false in case of an error + */ + public function owner() + { + if ($this->exists()) { + return fileowner($this->path); + } + + return false; + } + + /** + * Returns the file's group. + * + * @return int|false The file group, or false in case of an error + */ + public function group() + { + if ($this->exists()) { + return filegroup($this->path); + } + + return false; + } + + /** + * Returns last access time. + * + * @return int|false Timestamp of last access time, or false in case of an error + */ + public function lastAccess() + { + if ($this->exists()) { + return fileatime($this->path); + } + + return false; + } + + /** + * Returns last modified time. + * + * @return int|false Timestamp of last modification, or false in case of an error + */ + public function lastChange() + { + if ($this->exists()) { + return filemtime($this->path); + } + + return false; + } + + /** + * Returns the current folder. + * + * @return \Cake\Filesystem\Folder Current folder + */ + public function folder() + { + return $this->Folder; + } + + /** + * Copy the File to $dest + * + * @param string $dest Destination for the copy + * @param bool $overwrite Overwrite $dest if exists + * @return bool Success + */ + public function copy($dest, $overwrite = true) + { + if (!$this->exists() || is_file($dest) && !$overwrite) { + return false; + } + + return copy($this->path, $dest); + } + + /** + * Gets the mime type of the file. Uses the finfo extension if + * it's available, otherwise falls back to mime_content_type(). + * + * @return false|string The mimetype of the file, or false if reading fails. + */ + public function mime() + { + if (!$this->exists()) { + return false; + } + if (class_exists('finfo')) { + $finfo = new finfo(FILEINFO_MIME); + $type = $finfo->file($this->pwd()); + if (!$type) { + return false; + } + list($type) = explode(';', $type); + + return $type; + } + if (function_exists('mime_content_type')) { + return mime_content_type($this->pwd()); + } + + return false; + } + + /** + * Clear PHP's internal stat cache + * + * @param bool $all Clear all cache or not. Passing false will clear + * the stat cache for the current path only. + * @return void + */ + public function clearStatCache($all = false) + { + if ($all === false) { + clearstatcache(true, $this->path); + } + + clearstatcache(); + } + + /** + * Searches for a given text and replaces the text if found. + * + * @param string|array $search Text(s) to search for. + * @param string|array $replace Text(s) to replace with. + * @return bool Success + */ + public function replaceText($search, $replace) + { + if (!$this->open('r+')) { + return false; + } + + if ($this->lock !== null && flock($this->handle, LOCK_EX) === false) { + return false; + } + + $replaced = $this->write(str_replace($search, $replace, $this->read()), 'w', true); + + if ($this->lock !== null) { + flock($this->handle, LOCK_UN); + } + $this->close(); + + return $replaced; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Filesystem/Folder.php b/app/vendor/cakephp/cakephp/src/Filesystem/Folder.php new file mode 100644 index 000000000..a148782ae --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Filesystem/Folder.php @@ -0,0 +1,976 @@ + 'getPathname', + self::SORT_TIME => 'getCTime' + ]; + + /** + * Holds messages from last method. + * + * @var array + */ + protected $_messages = []; + + /** + * Holds errors from last method. + * + * @var array + */ + protected $_errors = []; + + /** + * Holds array of complete directory paths. + * + * @var array + */ + protected $_directories; + + /** + * Holds array of complete file paths. + * + * @var array + */ + protected $_files; + + /** + * Constructor. + * + * @param string|null $path Path to folder + * @param bool $create Create folder if not found + * @param int|false $mode Mode (CHMOD) to apply to created folder, false to ignore + */ + public function __construct($path = null, $create = false, $mode = false) + { + if (empty($path)) { + $path = TMP; + } + if ($mode) { + $this->mode = $mode; + } + + if (!file_exists($path) && $create === true) { + $this->create($path, $this->mode); + } + if (!Folder::isAbsolute($path)) { + $path = realpath($path); + } + if (!empty($path)) { + $this->cd($path); + } + } + + /** + * Return current path. + * + * @return string Current path + */ + public function pwd() + { + return $this->path; + } + + /** + * Change directory to $path. + * + * @param string $path Path to the directory to change to + * @return string|bool The new path. Returns false on failure + */ + public function cd($path) + { + $path = $this->realpath($path); + if ($path !== false && is_dir($path)) { + return $this->path = $path; + } + + return false; + } + + /** + * Returns an array of the contents of the current directory. + * The returned array holds two arrays: One of directories and one of files. + * + * @param string|bool $sort Whether you want the results sorted, set this and the sort property + * to false to get unsorted results. + * @param array|bool $exceptions Either an array or boolean true will not grab dot files + * @param bool $fullPath True returns the full path + * @return array Contents of current directory as an array, an empty array on failure + */ + public function read($sort = self::SORT_NAME, $exceptions = false, $fullPath = false) + { + $dirs = $files = []; + + if (!$this->pwd()) { + return [$dirs, $files]; + } + if (is_array($exceptions)) { + $exceptions = array_flip($exceptions); + } + $skipHidden = isset($exceptions['.']) || $exceptions === true; + + try { + $iterator = new DirectoryIterator($this->path); + } catch (Exception $e) { + return [$dirs, $files]; + } + + if (!is_bool($sort) && isset($this->_fsorts[$sort])) { + $methodName = $this->_fsorts[$sort]; + } else { + $methodName = $this->_fsorts[self::SORT_NAME]; + } + + foreach ($iterator as $item) { + if ($item->isDot()) { + continue; + } + $name = $item->getFilename(); + if ($skipHidden && $name[0] === '.' || isset($exceptions[$name])) { + continue; + } + if ($fullPath) { + $name = $item->getPathname(); + } + + if ($item->isDir()) { + $dirs[$item->{$methodName}()][] = $name; + } else { + $files[$item->{$methodName}()][] = $name; + } + } + + if ($sort || $this->sort) { + ksort($dirs); + ksort($files); + } + + if ($dirs) { + $dirs = array_merge(...array_values($dirs)); + } + + if ($files) { + $files = array_merge(...array_values($files)); + } + + return [$dirs, $files]; + } + + /** + * Returns an array of all matching files in current directory. + * + * @param string $regexpPattern Preg_match pattern (Defaults to: .*) + * @param bool $sort Whether results should be sorted. + * @return array Files that match given pattern + */ + public function find($regexpPattern = '.*', $sort = false) + { + list(, $files) = $this->read($sort); + + return array_values(preg_grep('/^' . $regexpPattern . '$/i', $files)); + } + + /** + * Returns an array of all matching files in and below current directory. + * + * @param string $pattern Preg_match pattern (Defaults to: .*) + * @param bool $sort Whether results should be sorted. + * @return array Files matching $pattern + */ + public function findRecursive($pattern = '.*', $sort = false) + { + if (!$this->pwd()) { + return []; + } + $startsOn = $this->path; + $out = $this->_findRecursive($pattern, $sort); + $this->cd($startsOn); + + return $out; + } + + /** + * Private helper function for findRecursive. + * + * @param string $pattern Pattern to match against + * @param bool $sort Whether results should be sorted. + * @return array Files matching pattern + */ + protected function _findRecursive($pattern, $sort = false) + { + list($dirs, $files) = $this->read($sort); + $found = []; + + foreach ($files as $file) { + if (preg_match('/^' . $pattern . '$/i', $file)) { + $found[] = Folder::addPathElement($this->path, $file); + } + } + $start = $this->path; + + foreach ($dirs as $dir) { + $this->cd(Folder::addPathElement($start, $dir)); + $found = array_merge($found, $this->findRecursive($pattern, $sort)); + } + + return $found; + } + + /** + * Returns true if given $path is a Windows path. + * + * @param string $path Path to check + * @return bool true if windows path, false otherwise + */ + public static function isWindowsPath($path) + { + return (preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) === '\\\\'); + } + + /** + * Returns true if given $path is an absolute path. + * + * @param string $path Path to check + * @return bool true if path is absolute. + */ + public static function isAbsolute($path) + { + if (empty($path)) { + return false; + } + + return $path[0] === '/' || + preg_match('/^[A-Z]:\\\\/i', $path) || + substr($path, 0, 2) === '\\\\' || + self::isRegisteredStreamWrapper($path); + } + + /** + * Returns true if given $path is a registered stream wrapper. + * + * @param string $path Path to check + * @return bool True if path is registered stream wrapper. + */ + public static function isRegisteredStreamWrapper($path) + { + return preg_match('/^[^:\/\/]+?(?=:\/\/)/', $path, $matches) && + in_array($matches[0], stream_get_wrappers()); + } + + /** + * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.) + * + * @param string $path Path to check + * @return string Set of slashes ("\\" or "/") + */ + public static function normalizePath($path) + { + return Folder::correctSlashFor($path); + } + + /** + * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.) + * + * @param string $path Path to check + * @return string Set of slashes ("\\" or "/") + */ + public static function correctSlashFor($path) + { + return Folder::isWindowsPath($path) ? '\\' : '/'; + } + + /** + * Returns $path with added terminating slash (corrected for Windows or other OS). + * + * @param string $path Path to check + * @return string Path with ending slash + */ + public static function slashTerm($path) + { + if (Folder::isSlashTerm($path)) { + return $path; + } + + return $path . Folder::correctSlashFor($path); + } + + /** + * Returns $path with $element added, with correct slash in-between. + * + * @param string $path Path + * @param string|array $element Element to add at end of path + * @return string Combined path + */ + public static function addPathElement($path, $element) + { + $element = (array)$element; + array_unshift($element, rtrim($path, DIRECTORY_SEPARATOR)); + + return implode(DIRECTORY_SEPARATOR, $element); + } + + /** + * Returns true if the Folder is in the given Cake path. + * + * @param string $path The path to check. + * @return bool + * @deprecated 3.2.12 This method will be removed in 4.0.0. Use inPath() instead. + */ + public function inCakePath($path = '') + { + deprecationWarning('Folder::inCakePath() is deprecated. Use Folder::inPath() instead.'); + $dir = substr(Folder::slashTerm(ROOT), 0, -1); + $newdir = $dir . $path; + + return $this->inPath($newdir); + } + + /** + * Returns true if the Folder is in the given path. + * + * @param string $path The absolute path to check that the current `pwd()` resides within. + * @param bool $reverse Reverse the search, check if the given `$path` resides within the current `pwd()`. + * @return bool + * @throws \InvalidArgumentException When the given `$path` argument is not an absolute path. + */ + public function inPath($path, $reverse = false) + { + if (!Folder::isAbsolute($path)) { + throw new InvalidArgumentException('The $path argument is expected to be an absolute path.'); + } + + $dir = Folder::slashTerm($path); + $current = Folder::slashTerm($this->pwd()); + + if (!$reverse) { + $return = preg_match('/^' . preg_quote($dir, '/') . '(.*)/', $current); + } else { + $return = preg_match('/^' . preg_quote($current, '/') . '(.*)/', $dir); + } + + return (bool)$return; + } + + /** + * Change the mode on a directory structure recursively. This includes changing the mode on files as well. + * + * @param string $path The path to chmod. + * @param int|bool $mode Octal value, e.g. 0755. + * @param bool $recursive Chmod recursively, set to false to only change the current directory. + * @param array $exceptions Array of files, directories to skip. + * @return bool Success. + */ + public function chmod($path, $mode = false, $recursive = true, array $exceptions = []) + { + if (!$mode) { + $mode = $this->mode; + } + + if ($recursive === false && is_dir($path)) { + //@codingStandardsIgnoreStart + if (@chmod($path, intval($mode, 8))) { + //@codingStandardsIgnoreEnd + $this->_messages[] = sprintf('%s changed to %s', $path, $mode); + + return true; + } + + $this->_errors[] = sprintf('%s NOT changed to %s', $path, $mode); + + return false; + } + + if (is_dir($path)) { + $paths = $this->tree($path); + + foreach ($paths as $type) { + foreach ($type as $fullpath) { + $check = explode(DIRECTORY_SEPARATOR, $fullpath); + $count = count($check); + + if (in_array($check[$count - 1], $exceptions)) { + continue; + } + + //@codingStandardsIgnoreStart + if (@chmod($fullpath, intval($mode, 8))) { + //@codingStandardsIgnoreEnd + $this->_messages[] = sprintf('%s changed to %s', $fullpath, $mode); + } else { + $this->_errors[] = sprintf('%s NOT changed to %s', $fullpath, $mode); + } + } + } + + if (empty($this->_errors)) { + return true; + } + } + + return false; + } + + /** + * Returns an array of subdirectories for the provided or current path. + * + * @param string|null $path The directory path to get subdirectories for. + * @param bool $fullPath Whether to return the full path or only the directory name. + * @return array Array of subdirectories for the provided or current path. + */ + public function subdirectories($path = null, $fullPath = true) + { + if (!$path) { + $path = $this->path; + } + $subdirectories = []; + + try { + $iterator = new DirectoryIterator($path); + } catch (Exception $e) { + return []; + } + + foreach ($iterator as $item) { + if (!$item->isDir() || $item->isDot()) { + continue; + } + $subdirectories[] = $fullPath ? $item->getRealPath() : $item->getFilename(); + } + + return $subdirectories; + } + + /** + * Returns an array of nested directories and files in each directory + * + * @param string|null $path the directory path to build the tree from + * @param array|bool $exceptions Either an array of files/folder to exclude + * or boolean true to not grab dot files/folders + * @param string|null $type either 'file' or 'dir'. Null returns both files and directories + * @return array Array of nested directories and files in each directory + */ + public function tree($path = null, $exceptions = false, $type = null) + { + if (!$path) { + $path = $this->path; + } + $files = []; + $directories = [$path]; + + if (is_array($exceptions)) { + $exceptions = array_flip($exceptions); + } + $skipHidden = false; + if ($exceptions === true) { + $skipHidden = true; + } elseif (isset($exceptions['.'])) { + $skipHidden = true; + unset($exceptions['.']); + } + + try { + $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_SELF); + $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); + } catch (Exception $e) { + if ($type === null) { + return [[], []]; + } + + return []; + } + + foreach ($iterator as $itemPath => $fsIterator) { + if ($skipHidden) { + $subPathName = $fsIterator->getSubPathname(); + if ($subPathName{0} === '.' || strpos($subPathName, DIRECTORY_SEPARATOR . '.') !== false) { + continue; + } + } + $item = $fsIterator->current(); + if (!empty($exceptions) && isset($exceptions[$item->getFilename()])) { + continue; + } + + if ($item->isFile()) { + $files[] = $itemPath; + } elseif ($item->isDir() && !$item->isDot()) { + $directories[] = $itemPath; + } + } + if ($type === null) { + return [$directories, $files]; + } + if ($type === 'dir') { + return $directories; + } + + return $files; + } + + /** + * Create a directory structure recursively. + * + * Can be used to create deep path structures like `/foo/bar/baz/shoe/horn` + * + * @param string $pathname The directory structure to create. Either an absolute or relative + * path. If the path is relative and exists in the process' cwd it will not be created. + * Otherwise relative paths will be prefixed with the current pwd(). + * @param int|bool $mode octal value 0755 + * @return bool Returns TRUE on success, FALSE on failure + */ + public function create($pathname, $mode = false) + { + if (is_dir($pathname) || empty($pathname)) { + return true; + } + + if (!self::isAbsolute($pathname)) { + $pathname = self::addPathElement($this->pwd(), $pathname); + } + + if (!$mode) { + $mode = $this->mode; + } + + if (is_file($pathname)) { + $this->_errors[] = sprintf('%s is a file', $pathname); + + return false; + } + $pathname = rtrim($pathname, DIRECTORY_SEPARATOR); + $nextPathname = substr($pathname, 0, strrpos($pathname, DIRECTORY_SEPARATOR)); + + if ($this->create($nextPathname, $mode)) { + if (!file_exists($pathname)) { + $old = umask(0); + if (mkdir($pathname, $mode, true)) { + umask($old); + $this->_messages[] = sprintf('%s created', $pathname); + + return true; + } + umask($old); + $this->_errors[] = sprintf('%s NOT created', $pathname); + + return false; + } + } + + return false; + } + + /** + * Returns the size in bytes of this Folder and its contents. + * + * @return int size in bytes of current folder + */ + public function dirsize() + { + $size = 0; + $directory = Folder::slashTerm($this->path); + $stack = [$directory]; + $count = count($stack); + for ($i = 0, $j = $count; $i < $j; $i++) { + if (is_file($stack[$i])) { + $size += filesize($stack[$i]); + } elseif (is_dir($stack[$i])) { + $dir = dir($stack[$i]); + if ($dir) { + while (($entry = $dir->read()) !== false) { + if ($entry === '.' || $entry === '..') { + continue; + } + $add = $stack[$i] . $entry; + + if (is_dir($stack[$i] . $entry)) { + $add = Folder::slashTerm($add); + } + $stack[] = $add; + } + $dir->close(); + } + } + $j = count($stack); + } + + return $size; + } + + /** + * Recursively Remove directories if the system allows. + * + * @param string|null $path Path of directory to delete + * @return bool Success + */ + public function delete($path = null) + { + if (!$path) { + $path = $this->pwd(); + } + if (!$path) { + return false; + } + $path = Folder::slashTerm($path); + if (is_dir($path)) { + try { + $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::CURRENT_AS_SELF); + $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::CHILD_FIRST); + } catch (Exception $e) { + return false; + } + + foreach ($iterator as $item) { + $filePath = $item->getPathname(); + if ($item->isFile() || $item->isLink()) { + //@codingStandardsIgnoreStart + if (@unlink($filePath)) { + //@codingStandardsIgnoreEnd + $this->_messages[] = sprintf('%s removed', $filePath); + } else { + $this->_errors[] = sprintf('%s NOT removed', $filePath); + } + } elseif ($item->isDir() && !$item->isDot()) { + //@codingStandardsIgnoreStart + if (@rmdir($filePath)) { + //@codingStandardsIgnoreEnd + $this->_messages[] = sprintf('%s removed', $filePath); + } else { + $this->_errors[] = sprintf('%s NOT removed', $filePath); + + return false; + } + } + } + + $path = rtrim($path, DIRECTORY_SEPARATOR); + //@codingStandardsIgnoreStart + if (@rmdir($path)) { + //@codingStandardsIgnoreEnd + $this->_messages[] = sprintf('%s removed', $path); + } else { + $this->_errors[] = sprintf('%s NOT removed', $path); + + return false; + } + } + + return true; + } + + /** + * Recursive directory copy. + * + * ### Options + * + * - `to` The directory to copy to. + * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd(). + * - `mode` The mode to copy the files/directories with as integer, e.g. 0775. + * - `skip` Files/directories to skip. + * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP + * - `recursive` Whether to copy recursively or not (default: true - recursive) + * + * @param array|string $options Either an array of options (see above) or a string of the destination directory. + * @return bool Success. + */ + public function copy($options) + { + if (!$this->pwd()) { + return false; + } + $to = null; + if (is_string($options)) { + $to = $options; + $options = []; + } + $options += [ + 'to' => $to, + 'from' => $this->path, + 'mode' => $this->mode, + 'skip' => [], + 'scheme' => Folder::MERGE, + 'recursive' => true + ]; + + $fromDir = $options['from']; + $toDir = $options['to']; + $mode = $options['mode']; + + if (!$this->cd($fromDir)) { + $this->_errors[] = sprintf('%s not found', $fromDir); + + return false; + } + + if (!is_dir($toDir)) { + $this->create($toDir, $mode); + } + + if (!is_writable($toDir)) { + $this->_errors[] = sprintf('%s not writable', $toDir); + + return false; + } + + $exceptions = array_merge(['.', '..', '.svn'], $options['skip']); + //@codingStandardsIgnoreStart + if ($handle = @opendir($fromDir)) { + //@codingStandardsIgnoreEnd + while (($item = readdir($handle)) !== false) { + $to = Folder::addPathElement($toDir, $item); + if (($options['scheme'] != Folder::SKIP || !is_dir($to)) && !in_array($item, $exceptions)) { + $from = Folder::addPathElement($fromDir, $item); + if (is_file($from) && (!is_file($to) || $options['scheme'] != Folder::SKIP)) { + if (copy($from, $to)) { + chmod($to, intval($mode, 8)); + touch($to, filemtime($from)); + $this->_messages[] = sprintf('%s copied to %s', $from, $to); + } else { + $this->_errors[] = sprintf('%s NOT copied to %s', $from, $to); + } + } + + if (is_dir($from) && file_exists($to) && $options['scheme'] === Folder::OVERWRITE) { + $this->delete($to); + } + + if (is_dir($from) && $options['recursive'] === false) { + continue; + } + + if (is_dir($from) && !file_exists($to)) { + $old = umask(0); + if (mkdir($to, $mode, true)) { + umask($old); + $old = umask(0); + chmod($to, $mode); + umask($old); + $this->_messages[] = sprintf('%s created', $to); + $options = ['to' => $to, 'from' => $from] + $options; + $this->copy($options); + } else { + $this->_errors[] = sprintf('%s not created', $to); + } + } elseif (is_dir($from) && $options['scheme'] === Folder::MERGE) { + $options = ['to' => $to, 'from' => $from] + $options; + $this->copy($options); + } + } + } + closedir($handle); + } else { + return false; + } + + return empty($this->_errors); + } + + /** + * Recursive directory move. + * + * ### Options + * + * - `to` The directory to copy to. + * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd(). + * - `chmod` The mode to copy the files/directories with. + * - `skip` Files/directories to skip. + * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP + * - `recursive` Whether to copy recursively or not (default: true - recursive) + * + * @param array|string $options (to, from, chmod, skip, scheme) + * @return bool Success + */ + public function move($options) + { + $to = null; + if (is_string($options)) { + $to = $options; + $options = (array)$options; + } + $options += ['to' => $to, 'from' => $this->path, 'mode' => $this->mode, 'skip' => [], 'recursive' => true]; + + if ($this->copy($options) && $this->delete($options['from'])) { + return (bool)$this->cd($options['to']); + } + + return false; + } + + /** + * get messages from latest method + * + * @param bool $reset Reset message stack after reading + * @return array + */ + public function messages($reset = true) + { + $messages = $this->_messages; + if ($reset) { + $this->_messages = []; + } + + return $messages; + } + + /** + * get error from latest method + * + * @param bool $reset Reset error stack after reading + * @return array + */ + public function errors($reset = true) + { + $errors = $this->_errors; + if ($reset) { + $this->_errors = []; + } + + return $errors; + } + + /** + * Get the real path (taking ".." and such into account) + * + * @param string $path Path to resolve + * @return string|false The resolved path + */ + public function realpath($path) + { + if (strpos($path, '..') === false) { + if (!Folder::isAbsolute($path)) { + $path = Folder::addPathElement($this->path, $path); + } + + return $path; + } + $path = str_replace('/', DIRECTORY_SEPARATOR, trim($path)); + $parts = explode(DIRECTORY_SEPARATOR, $path); + $newparts = []; + $newpath = ''; + if ($path[0] === DIRECTORY_SEPARATOR) { + $newpath = DIRECTORY_SEPARATOR; + } + + while (($part = array_shift($parts)) !== null) { + if ($part === '.' || $part === '') { + continue; + } + if ($part === '..') { + if (!empty($newparts)) { + array_pop($newparts); + continue; + } + + return false; + } + $newparts[] = $part; + } + $newpath .= implode(DIRECTORY_SEPARATOR, $newparts); + + return Folder::slashTerm($newpath); + } + + /** + * Returns true if given $path ends in a slash (i.e. is slash-terminated). + * + * @param string $path Path to check + * @return bool true if path ends with slash, false otherwise + */ + public static function isSlashTerm($path) + { + $lastChar = $path[strlen($path) - 1]; + + return $lastChar === '/' || $lastChar === '\\'; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Filesystem/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Filesystem/LICENSE.txt new file mode 100644 index 000000000..0c4b7932c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Filesystem/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2016, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/vendor/cakephp/cakephp/src/Filesystem/README.md b/app/vendor/cakephp/cakephp/src/Filesystem/README.md new file mode 100644 index 000000000..769837df4 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Filesystem/README.md @@ -0,0 +1,35 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/filesystem.svg?style=flat-square)](https://packagist.org/packages/cakephp/filesystem) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# CakePHP Filesystem Library + +The Folder and File utilities are convenience classes to help you read from and write/append to files; list files within a folder and other common directory related tasks. + +## Basic Usage + +Create a folder instance and search for all the `.ctp` files within it: + +```php +use Cake\Filesystem\Folder; + +$dir = new Folder('/path/to/folder'); +$files = $dir->find('.*\.ctp'); +``` + +Now you can loop through the files and read from or write/append to the contents or simply delete the file: + +```php +foreach ($files as $file) { + $file = new File($dir->pwd() . DIRECTORY_SEPARATOR . $file); + $contents = $file->read(); + // $file->write('I am overwriting the contents of this file'); + // $file->append('I am adding to the bottom of this file.'); + // $file->delete(); // I am deleting this file + $file->close(); // Be sure to close the file when you're done +} +``` + +## Documentation + +Please make sure you check the [official +documentation](https://book.cakephp.org/3.0/en/core-libraries/file-folder.html) diff --git a/app/vendor/cakephp/cakephp/src/Filesystem/composer.json b/app/vendor/cakephp/cakephp/src/Filesystem/composer.json new file mode 100644 index 000000000..e5dcb0702 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Filesystem/composer.json @@ -0,0 +1,34 @@ +{ + "name": "cakephp/filesystem", + "description": "CakePHP filesystem convenience classes to help you work with files and folders.", + "type": "library", + "keywords": [ + "cakephp", + "filesystem", + "files", + "folders" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/filesystem/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/filesystem" + }, + "require": { + "php": ">=5.6.0", + "cakephp/core": "^3.6.0" + }, + "autoload": { + "psr-4": { + "Cake\\Filesystem\\": "." + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Form/Form.php b/app/vendor/cakephp/cakephp/src/Form/Form.php new file mode 100644 index 000000000..5c2197b34 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Form/Form.php @@ -0,0 +1,322 @@ +schema()` and `$form->validator()`. + * + * Forms are conventionally placed in the `App\Form` namespace. + */ +class Form implements EventListenerInterface, EventDispatcherInterface, ValidatorAwareInterface +{ + /** + * Schema class. + * + * @var string + */ + protected $_schemaClass = Schema::class; + + use EventDispatcherTrait; + use ValidatorAwareTrait; + + /** + * The alias this object is assigned to validators as. + * + * @var string + */ + const VALIDATOR_PROVIDER_NAME = 'form'; + + /** + * The name of the event dispatched when a validator has been built. + * + * @var string + */ + const BUILD_VALIDATOR_EVENT = 'Form.buildValidator'; + + /** + * The schema used by this form. + * + * @var \Cake\Form\Schema + */ + protected $_schema; + + /** + * The errors if any + * + * @var array + */ + protected $_errors = []; + + /** + * The validator used by this form. + * + * @var \Cake\Validation\Validator + */ + protected $_validator; + + /** + * Constructor + * + * @param \Cake\Event\EventManager|null $eventManager The event manager. + * Defaults to a new instance. + */ + public function __construct(EventManager $eventManager = null) + { + if ($eventManager !== null) { + $this->setEventManager($eventManager); + } + + $this->getEventManager()->on($this); + } + + /** + * Get the Form callbacks this form is interested in. + * + * The conventional method map is: + * + * - Form.buildValidator => buildValidator + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Form.buildValidator' => 'buildValidator', + ]; + } + + /** + * Get/Set the schema for this form. + * + * This method will call `_buildSchema()` when the schema + * is first built. This hook method lets you configure the + * schema or load a pre-defined one. + * + * @param \Cake\Form\Schema|null $schema The schema to set, or null. + * @return \Cake\Form\Schema the schema instance. + */ + public function schema(Schema $schema = null) + { + if ($schema === null && empty($this->_schema)) { + $schema = $this->_buildSchema(new $this->_schemaClass); + } + if ($schema) { + $this->_schema = $schema; + } + + return $this->_schema; + } + + /** + * A hook method intended to be implemented by subclasses. + * + * You can use this method to define the schema using + * the methods on Cake\Form\Schema, or loads a pre-defined + * schema from a concrete class. + * + * @param \Cake\Form\Schema $schema The schema to customize. + * @return \Cake\Form\Schema The schema to use. + */ + protected function _buildSchema(Schema $schema) + { + return $schema; + } + + /** + * Get/Set the validator for this form. + * + * This method will call `_buildValidator()` when the validator + * is first built. This hook method lets you configure the + * validator or load a pre-defined one. + * + * @param \Cake\Validation\Validator|null $validator The validator to set, or null. + * @return \Cake\Validation\Validator the validator instance. + * @deprecated 3.6.0 Use Form::getValidator()/setValidator() instead. + */ + public function validator(Validator $validator = null) + { + deprecationWarning( + 'Form::validator() is deprecated. ' . + 'Use Form::getValidator()/setValidator() instead.' + ); + + if ($validator === null && empty($this->_validator)) { + $validator = $this->_buildValidator(new $this->_validatorClass); + } + if ($validator) { + $this->_validator = $validator; + $this->setValidator('default', $validator); + } + + return $this->getValidator(); + } + + /** + * A hook method intended to be implemented by subclasses. + * + * You can use this method to define the validator using + * the methods on Cake\Validation\Validator or loads a pre-defined + * validator from a concrete class. + * + * @param \Cake\Validation\Validator $validator The validator to customize. + * @return \Cake\Validation\Validator The validator to use. + * @deprecated 3.6.0 Use Form::getValidator()/setValidator() and buildValidator() instead. + */ + protected function _buildValidator(Validator $validator) + { + return $validator; + } + + /** + * Callback method for Form.buildValidator event. + * + * @param \Cake\Event\Event $event The Form.buildValidator event instance. + * @param \Cake\Validation\Validator $validator The validator to customize. + * @param string $name Validator name + * @return void + */ + public function buildValidator(Event $event, Validator $validator, $name) + { + $this->_buildValidator($validator); + } + + /** + * Used to check if $data passes this form's validation. + * + * @param array $data The data to check. + * @return bool Whether or not the data is valid. + */ + public function validate(array $data) + { + $validator = $this->getValidator(); + if (!$validator->count()) { + $method = new ReflectionMethod($this, 'validator'); + if ($method->getDeclaringClass()->getName() !== __CLASS__) { + $validator = $this->validator(); + } + } + $this->_errors = $validator->errors($data); + + return count($this->_errors) === 0; + } + + /** + * Get the errors in the form + * + * Will return the errors from the last call + * to `validate()` or `execute()`. + * + * @return array Last set validation errors. + */ + public function errors() + { + return $this->_errors; + } + + /** + * Set the errors in the form. + * + * ``` + * $errors = [ + * 'field_name' => ['rule_name' => 'message'] + * ]; + * + * $form->setErrors($errors); + * ``` + * + * @since 3.5.1 + * @param array $errors Errors list. + * @return $this + */ + public function setErrors(array $errors) + { + $this->_errors = $errors; + + return $this; + } + + /** + * Execute the form if it is valid. + * + * First validates the form, then calls the `_execute()` hook method. + * This hook method can be implemented in subclasses to perform + * the action of the form. This may be sending email, interacting + * with a remote API, or anything else you may need. + * + * @param array $data Form data. + * @return bool False on validation failure, otherwise returns the + * result of the `_execute()` method. + */ + public function execute(array $data) + { + if (!$this->validate($data)) { + return false; + } + + return $this->_execute($data); + } + + /** + * Hook method to be implemented in subclasses. + * + * Used by `execute()` to execute the form's action. + * + * @param array $data Form data. + * @return bool + */ + protected function _execute(array $data) + { + return true; + } + + /** + * Get the printable version of a Form instance. + * + * @return array + */ + public function __debugInfo() + { + $special = [ + '_schema' => $this->schema()->__debugInfo(), + '_errors' => $this->errors(), + '_validator' => $this->getValidator()->__debugInfo() + ]; + + return $special + get_object_vars($this); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Form/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Form/LICENSE.txt new file mode 100644 index 000000000..0c4b7932c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Form/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2016, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/vendor/cakephp/cakephp/src/Form/README.md b/app/vendor/cakephp/cakephp/src/Form/README.md new file mode 100644 index 000000000..a5b5af56e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Form/README.md @@ -0,0 +1,63 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/form.svg?style=flat-square)](https://packagist.org/packages/cakephp/form) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# CakePHP Form Library + +Form abstraction used to create forms not tied to ORM backed models, +or to other permanent datastores. Ideal for implementing forms on top of +API services, or contact forms. + +## Usage + + +```php +use Cake\Form\Form; +use Cake\Form\Schema; +use Cake\Validation\Validator; + +class ContactForm extends Form +{ + + protected function _buildSchema(Schema $schema) + { + return $schema->addField('name', 'string') + ->addField('email', ['type' => 'string']) + ->addField('body', ['type' => 'text']); + } + + public function validationDefault(Validator $validator) + { + return $validator->add('name', 'length', [ + 'rule' => ['minLength', 10], + 'message' => 'A name is required' + ])->add('email', 'format', [ + 'rule' => 'email', + 'message' => 'A valid email address is required', + ]); + } + + protected function _execute(array $data) + { + // Send an email. + return true; + } +} +``` + +In the above example we see the 3 hook methods that forms provide: + +- `_buildSchema()` is used to define the schema data. You can define field type, length, and precision. +- `validationDefault()` Gets a `Cake\Validation\Validator` instance that you can attach validators to. +- `_execute()` lets you define the behavior you want to happen when `execute()` is called and the data is valid. + +You can always define additional public methods as you need as well. + +```php +$contact = new ContactForm(); +$success = $contact->execute($data); +$errors = $contact->errors(); +``` + +## Documentation + +Please make sure you check the [official documentation](https://book.cakephp.org/3.0/en/core-libraries/form.html) diff --git a/app/vendor/cakephp/cakephp/src/Form/Schema.php b/app/vendor/cakephp/cakephp/src/Form/Schema.php new file mode 100644 index 000000000..08d11fcdb --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Form/Schema.php @@ -0,0 +1,142 @@ + null, + 'length' => null, + 'precision' => null, + 'default' => null, + ]; + + /** + * Add multiple fields to the schema. + * + * @param array $fields The fields to add. + * @return $this + */ + public function addFields(array $fields) + { + foreach ($fields as $name => $attrs) { + $this->addField($name, $attrs); + } + + return $this; + } + + /** + * Adds a field to the schema. + * + * @param string $name The field name. + * @param string|array $attrs The attributes for the field, or the type + * as a string. + * @return $this + */ + public function addField($name, $attrs) + { + if (is_string($attrs)) { + $attrs = ['type' => $attrs]; + } + $attrs = array_intersect_key($attrs, $this->_fieldDefaults); + $this->_fields[$name] = $attrs + $this->_fieldDefaults; + + return $this; + } + + /** + * Removes a field to the schema. + * + * @param string $name The field to remove. + * @return $this + */ + public function removeField($name) + { + unset($this->_fields[$name]); + + return $this; + } + + /** + * Get the list of fields in the schema. + * + * @return array The list of field names. + */ + public function fields() + { + return array_keys($this->_fields); + } + + /** + * Get the attributes for a given field. + * + * @param string $name The field name. + * @return null|array The attributes for a field, or null. + */ + public function field($name) + { + if (!isset($this->_fields[$name])) { + return null; + } + + return $this->_fields[$name]; + } + + /** + * Get the type of the named field. + * + * @param string $name The name of the field. + * @return string|null Either the field type or null if the + * field does not exist. + */ + public function fieldType($name) + { + $field = $this->field($name); + if (!$field) { + return null; + } + + return $field['type']; + } + + /** + * Get the printable version of this object + * + * @return array + */ + public function __debugInfo() + { + return [ + '_fields' => $this->_fields + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Form/composer.json b/app/vendor/cakephp/cakephp/src/Form/composer.json new file mode 100644 index 000000000..5e1dfdc92 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Form/composer.json @@ -0,0 +1,33 @@ +{ + "name": "cakephp/form", + "description": "CakePHP Form library", + "type": "library", + "keywords": [ + "cakephp", + "form" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/form/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/form" + }, + "require": { + "php": ">=5.6.0", + "cakephp/event": "^3.6.0", + "cakephp/validation": "^3.6.0" + }, + "autoload": { + "psr-4": { + "Cake\\Form\\": "." + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/ActionDispatcher.php b/app/vendor/cakephp/cakephp/src/Http/ActionDispatcher.php new file mode 100644 index 000000000..b8f40f329 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/ActionDispatcher.php @@ -0,0 +1,171 @@ +setEventManager($eventManager); + } + foreach ($filters as $filter) { + $this->addFilter($filter); + } + $this->factory = $factory ?: new ControllerFactory(); + } + + /** + * Dispatches a Request & Response + * + * @param \Cake\Http\ServerRequest $request The request to dispatch. + * @param \Cake\Http\Response $response The response to dispatch. + * @return \Cake\Http\Response A modified/replaced response. + * @throws \ReflectionException + */ + public function dispatch(ServerRequest $request, Response $response) + { + if (Router::getRequest(true) !== $request) { + Router::pushRequest($request); + } + $beforeEvent = $this->dispatchEvent('Dispatcher.beforeDispatch', compact('request', 'response')); + + $request = $beforeEvent->getData('request'); + if ($beforeEvent->getResult() instanceof Response) { + return $beforeEvent->getResult(); + } + + // Use the controller built by an beforeDispatch + // event handler if there is one. + if ($beforeEvent->getData('controller') instanceof Controller) { + $controller = $beforeEvent->getData('controller'); + } else { + $controller = $this->factory->create($request, $response); + } + + $response = $this->_invoke($controller); + if ($request->getParam('return')) { + return $response; + } + + $afterEvent = $this->dispatchEvent('Dispatcher.afterDispatch', compact('request', 'response')); + + return $afterEvent->getData('response'); + } + + /** + * Invoke a controller's action and wrapping methods. + * + * @param \Cake\Controller\Controller $controller The controller to invoke. + * @return \Cake\Http\Response The response + * @throws \LogicException If the controller action returns a non-response value. + */ + protected function _invoke(Controller $controller) + { + $this->dispatchEvent('Dispatcher.invokeController', ['controller' => $controller]); + + $result = $controller->startupProcess(); + if ($result instanceof Response) { + return $result; + } + + $response = $controller->invokeAction(); + if ($response !== null && !($response instanceof Response)) { + throw new LogicException('Controller actions can only return Cake\Http\Response or null.'); + } + + if (!$response && $controller->isAutoRenderEnabled()) { + $controller->render(); + } + + $result = $controller->shutdownProcess(); + if ($result instanceof Response) { + return $result; + } + if (!$response) { + $response = $controller->getResponse(); + } + + return $response; + } + + /** + * Add a filter to this dispatcher. + * + * The added filter will be attached to the event manager used + * by this dispatcher. + * + * @param \Cake\Event\EventListenerInterface $filter The filter to connect. Can be + * any EventListenerInterface. Typically an instance of \Cake\Routing\DispatcherFilter. + * @return void + * @deprecated This is only available for backwards compatibility with DispatchFilters + */ + public function addFilter(EventListenerInterface $filter) + { + deprecationWarning( + 'ActionDispatcher::addFilter() is deprecated. ' . + 'This is only available for backwards compatibility with DispatchFilters' + ); + + $this->filters[] = $filter; + $this->getEventManager()->on($filter); + } + + /** + * Get the connected filters. + * + * @return array + */ + public function getFilters() + { + return $this->filters; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/BaseApplication.php b/app/vendor/cakephp/cakephp/src/Http/BaseApplication.php new file mode 100644 index 000000000..b90ae008b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/BaseApplication.php @@ -0,0 +1,244 @@ +configDir = $configDir; + $this->plugins = Plugin::getCollection(); + $this->_eventManager = $eventManager ?: EventManager::instance(); + } + + /** + * @param \Cake\Http\MiddlewareQueue $middleware The middleware queue to set in your App Class + * @return \Cake\Http\MiddlewareQueue + */ + abstract public function middleware($middleware); + + /** + * {@inheritDoc} + */ + public function pluginMiddleware($middleware) + { + foreach ($this->plugins->with('middleware') as $plugin) { + $middleware = $plugin->middleware($middleware); + } + + return $middleware; + } + + /** + * {@inheritDoc} + */ + public function addPlugin($name, array $config = []) + { + if (is_string($name)) { + $plugin = $this->makePlugin($name, $config); + } else { + $plugin = $name; + } + if (!$plugin instanceof PluginInterface) { + throw new InvalidArgumentException("The `{$name}` plugin does not implement Cake\Core\PluginInterface."); + } + $this->plugins->add($plugin); + + return $this; + } + + /** + * Get the plugin collection in use. + * + * @return \Cake\Core\PluginCollection + */ + public function getPlugins() + { + return $this->plugins; + } + + /** + * Create a plugin instance from a classname and configuration + * + * @param string $name The plugin classname + * @param array $config Configuration options for the plugin + * @return \Cake\Core\PluginInterface + */ + protected function makePlugin($name, array $config) + { + $className = $name; + if (strpos($className, '\\') === false) { + $className = str_replace('/', '\\', $className) . '\\' . 'Plugin'; + } + if (class_exists($className)) { + return new $className($config); + } + + if (!isset($config['path'])) { + $config['path'] = $this->plugins->findPath($name); + } + $config['name'] = $name; + + return new BasePlugin($config); + } + + /** + * {@inheritDoc} + */ + public function bootstrap() + { + require_once $this->configDir . '/bootstrap.php'; + } + + /** + * {@inheritDoc} + */ + public function pluginBootstrap() + { + foreach ($this->plugins->with('bootstrap') as $plugin) { + $plugin->bootstrap($this); + } + } + + /** + * {@inheritDoc} + * + * By default this will load `config/routes.php` for ease of use and backwards compatibility. + * + * @param \Cake\Routing\RouteBuilder $routes A route builder to add routes into. + * @return void + */ + public function routes($routes) + { + if (!Router::$initialized) { + // Prevent routes from being loaded again + Router::$initialized = true; + + require $this->configDir . '/routes.php'; + } + } + + /** + * {@inheritDoc} + */ + public function pluginRoutes($routes) + { + foreach ($this->plugins->with('routes') as $plugin) { + $plugin->routes($routes); + } + + return $routes; + } + + /** + * Define the console commands for an application. + * + * By default all commands in CakePHP, plugins and the application will be + * loaded using conventions based names. + * + * @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into. + * @return \Cake\Console\CommandCollection The updated collection. + */ + public function console($commands) + { + return $commands->addMany($commands->autoDiscover()); + } + + /** + * {@inheritDoc} + */ + public function pluginConsole($commands) + { + foreach ($this->plugins->with('console') as $plugin) { + $commands = $plugin->console($commands); + } + + return $commands; + } + + /** + * Invoke the application. + * + * - Convert the PSR response into CakePHP equivalents. + * - Create the controller that will handle this request. + * - Invoke the controller. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request + * @param \Psr\Http\Message\ResponseInterface $response The response + * @param callable $next The next middleware + * @return \Psr\Http\Message\ResponseInterface + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) + { + return $this->getDispatcher()->dispatch($request, $response); + } + + /** + * Get the ActionDispatcher. + * + * @return \Cake\Http\ActionDispatcher + */ + protected function getDispatcher() + { + return new ActionDispatcher(null, $this->getEventManager(), DispatcherFactory::filters()); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/CallbackStream.php b/app/vendor/cakephp/cakephp/src/Http/CallbackStream.php new file mode 100644 index 000000000..f165f3123 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/CallbackStream.php @@ -0,0 +1,49 @@ +detach(); + $result = $callback ? $callback() : ''; + if (!is_string($result)) { + return ''; + } + + return $result; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Client.php b/app/vendor/cakephp/cakephp/src/Http/Client.php new file mode 100644 index 000000000..16dc8a23f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Client.php @@ -0,0 +1,614 @@ +get('/users', [], ['type' => 'json']); + * ``` + * + * The `type` option sets both the `Content-Type` and `Accept` header, to + * the same mime type. When using `type` you can use either a full mime + * type or an alias. If you need different types in the Accept and Content-Type + * headers you should set them manually and not use `type` + * + * ### Using authentication + * + * By using the `auth` key you can use authentication. The type sub option + * can be used to specify which authentication strategy you want to use. + * CakePHP comes with a few built-in strategies: + * + * - Basic + * - Digest + * - Oauth + * + * ### Using proxies + * + * By using the `proxy` key you can set authentication credentials for + * a proxy if you need to use one. The type sub option can be used to + * specify which authentication strategy you want to use. + * CakePHP comes with built-in support for basic authentication. + * + * @mixin \Cake\Core\InstanceConfigTrait + */ +class Client +{ + + use InstanceConfigTrait; + + /** + * Default configuration for the client. + * + * @var array + */ + protected $_defaultConfig = [ + 'adapter' => 'Cake\Http\Client\Adapter\Stream', + 'host' => null, + 'port' => null, + 'scheme' => 'http', + 'timeout' => 30, + 'ssl_verify_peer' => true, + 'ssl_verify_peer_name' => true, + 'ssl_verify_depth' => 5, + 'ssl_verify_host' => true, + 'redirect' => false, + ]; + + /** + * List of cookies from responses made with this client. + * + * Cookies are indexed by the cookie's domain or + * request host name. + * + * @var \Cake\Http\Cookie\CookieCollection + */ + protected $_cookies; + + /** + * Adapter for sending requests. Defaults to + * Cake\Http\Client\Adapter\Stream + * + * @var \Cake\Http\Client\Adapter\Stream + */ + protected $_adapter; + + /** + * Create a new HTTP Client. + * + * ### Config options + * + * You can set the following options when creating a client: + * + * - host - The hostname to do requests on. + * - port - The port to use. + * - scheme - The default scheme/protocol to use. Defaults to http. + * - timeout - The timeout in seconds. Defaults to 30 + * - ssl_verify_peer - Whether or not SSL certificates should be validated. + * Defaults to true. + * - ssl_verify_peer_name - Whether or not peer names should be validated. + * Defaults to true. + * - ssl_verify_depth - The maximum certificate chain depth to traverse. + * Defaults to 5. + * - ssl_verify_host - Verify that the certificate and hostname match. + * Defaults to true. + * - redirect - Number of redirects to follow. Defaults to false. + * + * @param array $config Config options for scoped clients. + */ + public function __construct($config = []) + { + $this->setConfig($config); + + $adapter = $this->_config['adapter']; + $this->setConfig('adapter', null); + if (is_string($adapter)) { + $adapter = new $adapter(); + } + $this->_adapter = $adapter; + + if (!empty($this->_config['cookieJar'])) { + $this->_cookies = $this->_config['cookieJar']; + $this->setConfig('cookieJar', null); + } else { + $this->_cookies = new CookieCollection(); + } + } + + /** + * Get the cookies stored in the Client. + * + * @return \Cake\Http\Client\CookieCollection + */ + public function cookies() + { + return $this->_cookies; + } + + /** + * Adds a cookie to the Client collection. + * + * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie object. + * @return $this + */ + public function addCookie(CookieInterface $cookie) + { + if (!$cookie->getDomain() || !$cookie->getPath()) { + throw new InvalidArgumentException('Cookie must have a domain and a path set.'); + } + $this->_cookies = $this->_cookies->add($cookie); + + return $this; + } + + /** + * Do a GET request. + * + * The $data argument supports a special `_content` key + * for providing a request body in a GET request. This is + * generally not used, but services like ElasticSearch use + * this feature. + * + * @param string $url The url or path you want to request. + * @param array $data The query data you want to send. + * @param array $options Additional options for the request. + * @return \Cake\Http\Client\Response + */ + public function get($url, $data = [], array $options = []) + { + $options = $this->_mergeOptions($options); + $body = null; + if (isset($data['_content'])) { + $body = $data['_content']; + unset($data['_content']); + } + $url = $this->buildUrl($url, $data, $options); + + return $this->_doRequest( + Request::METHOD_GET, + $url, + $body, + $options + ); + } + + /** + * Do a POST request. + * + * @param string $url The url or path you want to request. + * @param mixed $data The post data you want to send. + * @param array $options Additional options for the request. + * @return \Cake\Http\Client\Response + */ + public function post($url, $data = [], array $options = []) + { + $options = $this->_mergeOptions($options); + $url = $this->buildUrl($url, [], $options); + + return $this->_doRequest(Request::METHOD_POST, $url, $data, $options); + } + + /** + * Do a PUT request. + * + * @param string $url The url or path you want to request. + * @param mixed $data The request data you want to send. + * @param array $options Additional options for the request. + * @return \Cake\Http\Client\Response + */ + public function put($url, $data = [], array $options = []) + { + $options = $this->_mergeOptions($options); + $url = $this->buildUrl($url, [], $options); + + return $this->_doRequest(Request::METHOD_PUT, $url, $data, $options); + } + + /** + * Do a PATCH request. + * + * @param string $url The url or path you want to request. + * @param mixed $data The request data you want to send. + * @param array $options Additional options for the request. + * @return \Cake\Http\Client\Response + */ + public function patch($url, $data = [], array $options = []) + { + $options = $this->_mergeOptions($options); + $url = $this->buildUrl($url, [], $options); + + return $this->_doRequest(Request::METHOD_PATCH, $url, $data, $options); + } + + /** + * Do an OPTIONS request. + * + * @param string $url The url or path you want to request. + * @param mixed $data The request data you want to send. + * @param array $options Additional options for the request. + * @return \Cake\Http\Client\Response + */ + public function options($url, $data = [], array $options = []) + { + $options = $this->_mergeOptions($options); + $url = $this->buildUrl($url, [], $options); + + return $this->_doRequest(Request::METHOD_OPTIONS, $url, $data, $options); + } + + /** + * Do a TRACE request. + * + * @param string $url The url or path you want to request. + * @param mixed $data The request data you want to send. + * @param array $options Additional options for the request. + * @return \Cake\Http\Client\Response + */ + public function trace($url, $data = [], array $options = []) + { + $options = $this->_mergeOptions($options); + $url = $this->buildUrl($url, [], $options); + + return $this->_doRequest(Request::METHOD_TRACE, $url, $data, $options); + } + + /** + * Do a DELETE request. + * + * @param string $url The url or path you want to request. + * @param mixed $data The request data you want to send. + * @param array $options Additional options for the request. + * @return \Cake\Http\Client\Response + */ + public function delete($url, $data = [], array $options = []) + { + $options = $this->_mergeOptions($options); + $url = $this->buildUrl($url, [], $options); + + return $this->_doRequest(Request::METHOD_DELETE, $url, $data, $options); + } + + /** + * Do a HEAD request. + * + * @param string $url The url or path you want to request. + * @param array $data The query string data you want to send. + * @param array $options Additional options for the request. + * @return \Cake\Http\Client\Response + */ + public function head($url, array $data = [], array $options = []) + { + $options = $this->_mergeOptions($options); + $url = $this->buildUrl($url, $data, $options); + + return $this->_doRequest(Request::METHOD_HEAD, $url, '', $options); + } + + /** + * Helper method for doing non-GET requests. + * + * @param string $method HTTP method. + * @param string $url URL to request. + * @param mixed $data The request body. + * @param array $options The options to use. Contains auth, proxy, etc. + * @return \Cake\Http\Client\Response + */ + protected function _doRequest($method, $url, $data, $options) + { + $request = $this->_createRequest( + $method, + $url, + $data, + $options + ); + + return $this->send($request, $options); + } + + /** + * Does a recursive merge of the parameter with the scope config. + * + * @param array $options Options to merge. + * @return array Options merged with set config. + */ + protected function _mergeOptions($options) + { + return Hash::merge($this->_config, $options); + } + + /** + * Send a request. + * + * Used internally by other methods, but can also be used to send + * handcrafted Request objects. + * + * @param \Cake\Http\Client\Request $request The request to send. + * @param array $options Additional options to use. + * @return \Cake\Http\Client\Response + */ + public function send(Request $request, $options = []) + { + $redirects = 0; + if (isset($options['redirect'])) { + $redirects = (int)$options['redirect']; + unset($options['redirect']); + } + + do { + $response = $this->_sendRequest($request, $options); + + $handleRedirect = $response->isRedirect() && $redirects-- > 0; + if ($handleRedirect) { + $url = $request->getUri(); + $request = $this->_cookies->addToRequest($request, []); + + $location = $response->getHeaderLine('Location'); + $locationUrl = $this->buildUrl($location, [], [ + 'host' => $url->getHost(), + 'port' => $url->getPort(), + 'scheme' => $url->getScheme(), + 'protocolRelative' => true + ]); + + $request = $request->withUri(new Uri($locationUrl)); + } + } while ($handleRedirect); + + return $response; + } + + /** + * Send a request without redirection. + * + * @param \Cake\Http\Client\Request $request The request to send. + * @param array $options Additional options to use. + * @return \Cake\Http\Client\Response + */ + protected function _sendRequest(Request $request, $options) + { + $responses = $this->_adapter->send($request, $options); + $url = $request->getUri(); + foreach ($responses as $response) { + $this->_cookies = $this->_cookies->addFromResponse($response, $request); + } + + return array_pop($responses); + } + + /** + * Generate a URL based on the scoped client options. + * + * @param string $url Either a full URL or just the path. + * @param string|array $query The query data for the URL. + * @param array $options The config options stored with Client::config() + * @return string A complete url with scheme, port, host, and path. + */ + public function buildUrl($url, $query = [], $options = []) + { + if (empty($options) && empty($query)) { + return $url; + } + if ($query) { + $q = (strpos($url, '?') === false) ? '?' : '&'; + $url .= $q; + $url .= is_string($query) ? $query : http_build_query($query); + } + $defaults = [ + 'host' => null, + 'port' => null, + 'scheme' => 'http', + 'protocolRelative' => false + ]; + $options += $defaults; + + if ($options['protocolRelative'] && preg_match('#^//#', $url)) { + $url = $options['scheme'] . ':' . $url; + } + if (preg_match('#^https?://#', $url)) { + return $url; + } + + $defaultPorts = [ + 'http' => 80, + 'https' => 443 + ]; + $out = $options['scheme'] . '://' . $options['host']; + if ($options['port'] && $options['port'] != $defaultPorts[$options['scheme']]) { + $out .= ':' . $options['port']; + } + $out .= '/' . ltrim($url, '/'); + + return $out; + } + + /** + * Creates a new request object based on the parameters. + * + * @param string $method HTTP method name. + * @param string $url The url including query string. + * @param mixed $data The request body. + * @param array $options The options to use. Contains auth, proxy, etc. + * @return \Cake\Http\Client\Request + */ + protected function _createRequest($method, $url, $data, $options) + { + $headers = isset($options['headers']) ? (array)$options['headers'] : []; + if (isset($options['type'])) { + $headers = array_merge($headers, $this->_typeHeaders($options['type'])); + } + if (is_string($data) && !isset($headers['Content-Type']) && !isset($headers['content-type'])) { + $headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + $request = new Request($url, $method, $headers, $data); + $cookies = isset($options['cookies']) ? $options['cookies'] : []; + /** @var \Cake\Http\Client\Request $request */ + $request = $this->_cookies->addToRequest($request, $cookies); + if (isset($options['auth'])) { + $request = $this->_addAuthentication($request, $options); + } + if (isset($options['proxy'])) { + $request = $this->_addProxy($request, $options); + } + + return $request; + } + + /** + * Returns headers for Accept/Content-Type based on a short type + * or full mime-type. + * + * @param string $type short type alias or full mimetype. + * @return array Headers to set on the request. + * @throws \Cake\Core\Exception\Exception When an unknown type alias is used. + */ + protected function _typeHeaders($type) + { + if (strpos($type, '/') !== false) { + return [ + 'Accept' => $type, + 'Content-Type' => $type + ]; + } + $typeMap = [ + 'json' => 'application/json', + 'xml' => 'application/xml', + ]; + if (!isset($typeMap[$type])) { + throw new Exception("Unknown type alias '$type'."); + } + + return [ + 'Accept' => $typeMap[$type], + 'Content-Type' => $typeMap[$type], + ]; + } + + /** + * Add authentication headers to the request. + * + * Uses the authentication type to choose the correct strategy + * and use its methods to add headers. + * + * @param \Cake\Http\Client\Request $request The request to modify. + * @param array $options Array of options containing the 'auth' key. + * @return \Cake\Http\Client\Request The updated request object. + */ + protected function _addAuthentication(Request $request, $options) + { + $auth = $options['auth']; + $adapter = $this->_createAuth($auth, $options); + $result = $adapter->authentication($request, $options['auth']); + + return $result ?: $request; + } + + /** + * Add proxy authentication headers. + * + * Uses the authentication type to choose the correct strategy + * and use its methods to add headers. + * + * @param \Cake\Http\Client\Request $request The request to modify. + * @param array $options Array of options containing the 'proxy' key. + * @return \Cake\Http\Client\Request The updated request object. + */ + protected function _addProxy(Request $request, $options) + { + $auth = $options['proxy']; + $adapter = $this->_createAuth($auth, $options); + $result = $adapter->proxyAuthentication($request, $options['proxy']); + + return $result ?: $request; + } + + /** + * Create the authentication strategy. + * + * Use the configuration options to create the correct + * authentication strategy handler. + * + * @param array $auth The authentication options to use. + * @param array $options The overall request options to use. + * @return mixed Authentication strategy instance. + * @throws \Cake\Core\Exception\Exception when an invalid strategy is chosen. + */ + protected function _createAuth($auth, $options) + { + if (empty($auth['type'])) { + $auth['type'] = 'basic'; + } + $name = ucfirst($auth['type']); + $class = App::className($name, 'Http/Client/Auth'); + if (!$class) { + throw new Exception( + sprintf('Invalid authentication type %s', $name) + ); + } + + return new $class($this, $options); + } +} +// @deprecated Backwards compatibility with earler 3.x versions. +class_alias('Cake\Http\Client', 'Cake\Network\Http\Client'); diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Adapter/Stream.php b/app/vendor/cakephp/cakephp/src/Http/Client/Adapter/Stream.php new file mode 100644 index 000000000..21cbe972f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Client/Adapter/Stream.php @@ -0,0 +1,334 @@ +_stream = null; + $this->_context = null; + $this->_contextOptions = []; + $this->_sslContextOptions = []; + $this->_connectionErrors = []; + + $this->_buildContext($request, $options); + + return $this->_send($request); + } + + /** + * Create the response list based on the headers & content + * + * Creates one or many response objects based on the number + * of redirects that occurred. + * + * @param array $headers The list of headers from the request(s) + * @param string $content The response content. + * @return \Cake\Http\Client\Response[] The list of responses from the request(s) + */ + public function createResponses($headers, $content) + { + $indexes = $responses = []; + foreach ($headers as $i => $header) { + if (strtoupper(substr($header, 0, 5)) === 'HTTP/') { + $indexes[] = $i; + } + } + $last = count($indexes) - 1; + foreach ($indexes as $i => $start) { + $end = isset($indexes[$i + 1]) ? $indexes[$i + 1] - $start : null; + $headerSlice = array_slice($headers, $start, $end); + $body = $i == $last ? $content : ''; + $responses[] = $this->_buildResponse($headerSlice, $body); + } + + return $responses; + } + + /** + * Build the stream context out of the request object. + * + * @param \Cake\Http\Client\Request $request The request to build context from. + * @param array $options Additional request options. + * @return void + */ + protected function _buildContext(Request $request, $options) + { + $this->_buildContent($request, $options); + $this->_buildHeaders($request, $options); + $this->_buildOptions($request, $options); + + $url = $request->getUri(); + $scheme = parse_url($url, PHP_URL_SCHEME); + if ($scheme === 'https') { + $this->_buildSslContext($request, $options); + } + $this->_context = stream_context_create([ + 'http' => $this->_contextOptions, + 'ssl' => $this->_sslContextOptions, + ]); + } + + /** + * Build the header context for the request. + * + * Creates cookies & headers. + * + * @param \Cake\Http\Client\Request $request The request being sent. + * @param array $options Array of options to use. + * @return void + */ + protected function _buildHeaders(Request $request, $options) + { + $headers = []; + foreach ($request->getHeaders() as $name => $values) { + $headers[] = sprintf('%s: %s', $name, implode(', ', $values)); + } + $this->_contextOptions['header'] = implode("\r\n", $headers); + } + + /** + * Builds the request content based on the request object. + * + * If the $request->body() is a string, it will be used as is. + * Array data will be processed with Cake\Http\Client\FormData + * + * @param \Cake\Http\Client\Request $request The request being sent. + * @param array $options Array of options to use. + * @return void + */ + protected function _buildContent(Request $request, $options) + { + $body = $request->getBody(); + if (empty($body)) { + $this->_contextOptions['content'] = ''; + + return; + } + $body->rewind(); + $this->_contextOptions['content'] = $body->getContents(); + } + + /** + * Build miscellaneous options for the request. + * + * @param \Cake\Http\Client\Request $request The request being sent. + * @param array $options Array of options to use. + * @return void + */ + protected function _buildOptions(Request $request, $options) + { + $this->_contextOptions['method'] = $request->getMethod(); + $this->_contextOptions['protocol_version'] = $request->getProtocolVersion(); + $this->_contextOptions['ignore_errors'] = true; + + if (isset($options['timeout'])) { + $this->_contextOptions['timeout'] = $options['timeout']; + } + // Redirects are handled in the client layer because of cookie handling issues. + $this->_contextOptions['max_redirects'] = 0; + + if (isset($options['proxy']['proxy'])) { + $this->_contextOptions['request_fulluri'] = true; + $this->_contextOptions['proxy'] = $options['proxy']['proxy']; + } + } + + /** + * Build SSL options for the request. + * + * @param \Cake\Http\Client\Request $request The request being sent. + * @param array $options Array of options to use. + * @return void + */ + protected function _buildSslContext(Request $request, $options) + { + $sslOptions = [ + 'ssl_verify_peer', + 'ssl_verify_peer_name', + 'ssl_verify_depth', + 'ssl_allow_self_signed', + 'ssl_cafile', + 'ssl_local_cert', + 'ssl_passphrase', + ]; + if (empty($options['ssl_cafile'])) { + $options['ssl_cafile'] = CORE_PATH . 'config' . DIRECTORY_SEPARATOR . 'cacert.pem'; + } + if (!empty($options['ssl_verify_host'])) { + $url = $request->getUri(); + $host = parse_url($url, PHP_URL_HOST); + $this->_sslContextOptions['peer_name'] = $host; + } + foreach ($sslOptions as $key) { + if (isset($options[$key])) { + $name = substr($key, 4); + $this->_sslContextOptions[$name] = $options[$key]; + } + } + } + + /** + * Open the stream and send the request. + * + * @param \Cake\Http\Client\Request $request The request object. + * @return array Array of populated Response objects + * @throws \Cake\Http\Exception\HttpException + */ + protected function _send(Request $request) + { + $deadline = false; + if (isset($this->_contextOptions['timeout']) && $this->_contextOptions['timeout'] > 0) { + $deadline = time() + $this->_contextOptions['timeout']; + } + + $url = $request->getUri(); + $this->_open($url); + $content = ''; + $timedOut = false; + + while (!feof($this->_stream)) { + if ($deadline !== false) { + stream_set_timeout($this->_stream, max($deadline - time(), 1)); + } + + $content .= fread($this->_stream, 8192); + + $meta = stream_get_meta_data($this->_stream); + if ($meta['timed_out'] || ($deadline !== false && time() > $deadline)) { + $timedOut = true; + break; + } + } + $meta = stream_get_meta_data($this->_stream); + fclose($this->_stream); + + if ($timedOut) { + throw new HttpException('Connection timed out ' . $url, 504); + } + + $headers = $meta['wrapper_data']; + if (isset($headers['headers']) && is_array($headers['headers'])) { + $headers = $headers['headers']; + } + + return $this->createResponses($headers, $content); + } + + /** + * Build a response object + * + * @param array $headers Unparsed headers. + * @param string $body The response body. + * + * @return \Cake\Http\Client\Response + */ + protected function _buildResponse($headers, $body) + { + return new Response($headers, $body); + } + + /** + * Open the socket and handle any connection errors. + * + * @param string $url The url to connect to. + * @return void + * @throws \Cake\Core\Exception\Exception + */ + protected function _open($url) + { + set_error_handler(function ($code, $message) { + $this->_connectionErrors[] = $message; + }); + try { + $this->_stream = fopen($url, 'rb', false, $this->_context); + } finally { + restore_error_handler(); + } + + if (!$this->_stream || !empty($this->_connectionErrors)) { + throw new Exception(implode("\n", $this->_connectionErrors)); + } + } + + /** + * Get the context options + * + * Useful for debugging and testing context creation. + * + * @return array + */ + public function contextOptions() + { + return array_merge($this->_contextOptions, $this->_sslContextOptions); + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Http\Client\Adapter\Stream', 'Cake\Network\Http\Adapter\Stream'); diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Basic.php b/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Basic.php new file mode 100644 index 000000000..6debf1bcd --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Basic.php @@ -0,0 +1,77 @@ +_generateHeader($credentials['username'], $credentials['password']); + $request = $request->withHeader('Authorization', $value); + } + + return $request; + } + + /** + * Proxy Authentication + * + * @param \Cake\Http\Client\Request $request Request instance. + * @param array $credentials Credentials. + * @return \Cake\Http\Client\Request The updated request. + * @see https://www.ietf.org/rfc/rfc2617.txt + */ + public function proxyAuthentication(Request $request, array $credentials) + { + if (isset($credentials['username'], $credentials['password'])) { + $value = $this->_generateHeader($credentials['username'], $credentials['password']); + $request = $request->withHeader('Proxy-Authorization', $value); + } + + return $request; + } + + /** + * Generate basic [proxy] authentication header + * + * @param string $user Username. + * @param string $pass Password. + * @return string + */ + protected function _generateHeader($user, $pass) + { + return 'Basic ' . base64_encode($user . ':' . $pass); + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Http\Client\Auth\Basic', 'Cake\Network\Http\Auth\Basic'); diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Digest.php b/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Digest.php new file mode 100644 index 000000000..9d4256479 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Digest.php @@ -0,0 +1,148 @@ +_client = $client; + } + + /** + * Add Authorization header to the request. + * + * @param \Cake\Http\Client\Request $request The request object. + * @param array $credentials Authentication credentials. + * @return \Cake\Http\Client\Request The updated request. + * @see https://www.ietf.org/rfc/rfc2617.txt + */ + public function authentication(Request $request, array $credentials) + { + if (!isset($credentials['username'], $credentials['password'])) { + return $request; + } + if (!isset($credentials['realm'])) { + $credentials = $this->_getServerInfo($request, $credentials); + } + if (!isset($credentials['realm'])) { + return $request; + } + $value = $this->_generateHeader($request, $credentials); + + return $request->withHeader('Authorization', $value); + } + + /** + * Retrieve information about the authentication + * + * Will get the realm and other tokens by performing + * another request without authentication to get authentication + * challenge. + * + * @param \Cake\Http\Client\Request $request The request object. + * @param array $credentials Authentication credentials. + * @return array modified credentials. + */ + protected function _getServerInfo(Request $request, $credentials) + { + $response = $this->_client->get( + $request->getUri(), + [], + ['auth' => ['type' => null]] + ); + + if (!$response->getHeader('WWW-Authenticate')) { + return []; + } + preg_match_all( + '@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', + $response->getHeaderLine('WWW-Authenticate'), + $matches, + PREG_SET_ORDER + ); + foreach ($matches as $match) { + $credentials[$match[1]] = $match[2]; + } + if (!empty($credentials['qop']) && empty($credentials['nc'])) { + $credentials['nc'] = 1; + } + + return $credentials; + } + + /** + * Generate the header Authorization + * + * @param \Cake\Http\Client\Request $request The request object. + * @param array $credentials Authentication credentials. + * @return string + */ + protected function _generateHeader(Request $request, $credentials) + { + $path = $request->getUri()->getPath(); + $a1 = md5($credentials['username'] . ':' . $credentials['realm'] . ':' . $credentials['password']); + $a2 = md5($request->getMethod() . ':' . $path); + $nc = null; + + if (empty($credentials['qop'])) { + $response = md5($a1 . ':' . $credentials['nonce'] . ':' . $a2); + } else { + $credentials['cnonce'] = uniqid(); + $nc = sprintf('%08x', $credentials['nc']++); + $response = md5($a1 . ':' . $credentials['nonce'] . ':' . $nc . ':' . $credentials['cnonce'] . ':auth:' . $a2); + } + + $authHeader = 'Digest '; + $authHeader .= 'username="' . str_replace(['\\', '"'], ['\\\\', '\\"'], $credentials['username']) . '", '; + $authHeader .= 'realm="' . $credentials['realm'] . '", '; + $authHeader .= 'nonce="' . $credentials['nonce'] . '", '; + $authHeader .= 'uri="' . $path . '", '; + $authHeader .= 'response="' . $response . '"'; + if (!empty($credentials['opaque'])) { + $authHeader .= ', opaque="' . $credentials['opaque'] . '"'; + } + if (!empty($credentials['qop'])) { + $authHeader .= ', qop="auth", nc=' . $nc . ', cnonce="' . $credentials['cnonce'] . '"'; + } + + return $authHeader; + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Http\Client\Auth\Digest', 'Cake\Network\Http\Auth\Digest'); diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Oauth.php b/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Oauth.php new file mode 100644 index 000000000..597e853e3 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Oauth.php @@ -0,0 +1,368 @@ +_hmacSha1($request, $credentials); + break; + + case 'RSA-SHA1': + if (!isset($credentials['privateKey'])) { + return $request; + } + $value = $this->_rsaSha1($request, $credentials); + break; + + case 'PLAINTEXT': + $hasKeys = isset( + $credentials['consumerSecret'], + $credentials['token'], + $credentials['tokenSecret'] + ); + if (!$hasKeys) { + return $request; + } + $value = $this->_plaintext($request, $credentials); + break; + + default: + throw new Exception(sprintf('Unknown Oauth signature method %s', $credentials['method'])); + } + + return $request->withHeader('Authorization', $value); + } + + /** + * Plaintext signing + * + * This method is **not** suitable for plain HTTP. + * You should only ever use PLAINTEXT when dealing with SSL + * services. + * + * @param \Cake\Http\Client\Request $request The request object. + * @param array $credentials Authentication credentials. + * @return string Authorization header. + */ + protected function _plaintext($request, $credentials) + { + $values = [ + 'oauth_version' => '1.0', + 'oauth_nonce' => uniqid(), + 'oauth_timestamp' => time(), + 'oauth_signature_method' => 'PLAINTEXT', + 'oauth_token' => $credentials['token'], + 'oauth_consumer_key' => $credentials['consumerKey'], + ]; + if (isset($credentials['realm'])) { + $values['oauth_realm'] = $credentials['realm']; + } + $key = [$credentials['consumerSecret'], $credentials['tokenSecret']]; + $key = implode('&', $key); + $values['oauth_signature'] = $key; + + return $this->_buildAuth($values); + } + + /** + * Use HMAC-SHA1 signing. + * + * This method is suitable for plain HTTP or HTTPS. + * + * @param \Cake\Http\Client\Request $request The request object. + * @param array $credentials Authentication credentials. + * @return string + */ + protected function _hmacSha1($request, $credentials) + { + $nonce = isset($credentials['nonce']) ? $credentials['nonce'] : uniqid(); + $timestamp = isset($credentials['timestamp']) ? $credentials['timestamp'] : time(); + $values = [ + 'oauth_version' => '1.0', + 'oauth_nonce' => $nonce, + 'oauth_timestamp' => $timestamp, + 'oauth_signature_method' => 'HMAC-SHA1', + 'oauth_token' => $credentials['token'], + 'oauth_consumer_key' => $credentials['consumerKey'], + ]; + $baseString = $this->baseString($request, $values); + + if (isset($credentials['realm'])) { + $values['oauth_realm'] = $credentials['realm']; + } + $key = [$credentials['consumerSecret'], $credentials['tokenSecret']]; + $key = array_map([$this, '_encode'], $key); + $key = implode('&', $key); + + $values['oauth_signature'] = base64_encode( + hash_hmac('sha1', $baseString, $key, true) + ); + + return $this->_buildAuth($values); + } + + /** + * Use RSA-SHA1 signing. + * + * This method is suitable for plain HTTP or HTTPS. + * + * @param \Cake\Http\Client\Request $request The request object. + * @param array $credentials Authentication credentials. + * @return string + * + * @throws \RuntimeException + */ + protected function _rsaSha1($request, $credentials) + { + if (!function_exists('openssl_pkey_get_private')) { + throw new RuntimeException('RSA-SHA1 signature method requires the OpenSSL extension.'); + } + + $nonce = isset($credentials['nonce']) ? $credentials['nonce'] : bin2hex(Security::randomBytes(16)); + $timestamp = isset($credentials['timestamp']) ? $credentials['timestamp'] : time(); + $values = [ + 'oauth_version' => '1.0', + 'oauth_nonce' => $nonce, + 'oauth_timestamp' => $timestamp, + 'oauth_signature_method' => 'RSA-SHA1', + 'oauth_consumer_key' => $credentials['consumerKey'], + ]; + if (isset($credentials['consumerSecret'])) { + $values['oauth_consumer_secret'] = $credentials['consumerSecret']; + } + if (isset($credentials['token'])) { + $values['oauth_token'] = $credentials['token']; + } + if (isset($credentials['tokenSecret'])) { + $values['oauth_token_secret'] = $credentials['tokenSecret']; + } + $baseString = $this->baseString($request, $values); + + if (isset($credentials['realm'])) { + $values['oauth_realm'] = $credentials['realm']; + } + + if (is_resource($credentials['privateKey'])) { + $resource = $credentials['privateKey']; + $privateKey = stream_get_contents($resource); + rewind($resource); + $credentials['privateKey'] = $privateKey; + } + + $credentials += [ + 'privateKeyPassphrase' => null, + ]; + if (is_resource($credentials['privateKeyPassphrase'])) { + $resource = $credentials['privateKeyPassphrase']; + $passphrase = stream_get_line($resource, 0, PHP_EOL); + rewind($resource); + $credentials['privateKeyPassphrase'] = $passphrase; + } + $privateKey = openssl_pkey_get_private($credentials['privateKey'], $credentials['privateKeyPassphrase']); + $signature = ''; + openssl_sign($baseString, $signature, $privateKey); + openssl_free_key($privateKey); + + $values['oauth_signature'] = base64_encode($signature); + + return $this->_buildAuth($values); + } + + /** + * Generate the Oauth basestring + * + * - Querystring, request data and oauth_* parameters are combined. + * - Values are sorted by name and then value. + * - Request values are concatenated and urlencoded. + * - The request URL (without querystring) is normalized. + * - The HTTP method, URL and request parameters are concatenated and returned. + * + * @param \Cake\Http\Client\Request $request The request object. + * @param array $oauthValues Oauth values. + * @return string + */ + public function baseString($request, $oauthValues) + { + $parts = [ + $request->getMethod(), + $this->_normalizedUrl($request->getUri()), + $this->_normalizedParams($request, $oauthValues), + ]; + $parts = array_map([$this, '_encode'], $parts); + + return implode('&', $parts); + } + + /** + * Builds a normalized URL + * + * Section 9.1.2. of the Oauth spec + * + * @param \Psr\Http\Message\UriInterface $uri Uri object to build a normalized version of. + * @return string Normalized URL + */ + protected function _normalizedUrl($uri) + { + $out = $uri->getScheme() . '://'; + $out .= strtolower($uri->getHost()); + $out .= $uri->getPath(); + + return $out; + } + + /** + * Sorts and normalizes request data and oauthValues + * + * Section 9.1.1 of Oauth spec. + * + * - URL encode keys + values. + * - Sort keys & values by byte value. + * + * @param \Cake\Http\Client\Request $request The request object. + * @param array $oauthValues Oauth values. + * @return string sorted and normalized values + */ + protected function _normalizedParams($request, $oauthValues) + { + $query = parse_url($request->getUri(), PHP_URL_QUERY); + parse_str($query, $queryArgs); + + $post = []; + $body = $request->body(); + if (is_string($body) && $request->getHeaderLine('content-type') === 'application/x-www-form-urlencoded') { + parse_str($body, $post); + } + if (is_array($body)) { + $post = $body; + } + + $args = array_merge($queryArgs, $oauthValues, $post); + $pairs = $this->_normalizeData($args); + $data = []; + foreach ($pairs as $pair) { + $data[] = implode('=', $pair); + } + sort($data, SORT_STRING); + + return implode('&', $data); + } + + /** + * Recursively convert request data into the normalized form. + * + * @param array $args The arguments to normalize. + * @param string $path The current path being converted. + * @see https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 + * @return array + */ + protected function _normalizeData($args, $path = '') + { + $data = []; + foreach ($args as $key => $value) { + if ($path) { + // Fold string keys with []. + // Numeric keys result in a=b&a=c. While this isn't + // standard behavior in PHP, it is common in other platforms. + if (!is_numeric($key)) { + $key = "{$path}[{$key}]"; + } else { + $key = $path; + } + } + if (is_array($value)) { + uksort($value, 'strcmp'); + $data = array_merge($data, $this->_normalizeData($value, $key)); + } else { + $data[] = [$key, $value]; + } + } + + return $data; + } + + /** + * Builds the Oauth Authorization header value. + * + * @param array $data The oauth_* values to build + * @return string + */ + protected function _buildAuth($data) + { + $out = 'OAuth '; + $params = []; + foreach ($data as $key => $value) { + $params[] = $key . '="' . $this->_encode($value) . '"'; + } + $out .= implode(',', $params); + + return $out; + } + + /** + * URL Encodes a value based on rules of rfc3986 + * + * @param string $value Value to encode. + * @return string + */ + protected function _encode($value) + { + return str_replace(['%7E', '+'], ['~', ' '], rawurlencode($value)); + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Http\Client\Auth\Oauth', 'Cake\Network\Http\Auth\Oauth'); diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/CookieCollection.php b/app/vendor/cakephp/cakephp/src/Http/Client/CookieCollection.php new file mode 100644 index 000000000..817bb063c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Client/CookieCollection.php @@ -0,0 +1,121 @@ +getHeader('Set-Cookie'); + $cookies = $this->parseSetCookieHeader($header); + $cookies = $this->setRequestDefaults($cookies, $host, $path); + foreach ($cookies as $cookie) { + $this->cookies[$cookie->getId()] = $cookie; + } + $this->removeExpiredCookies($host, $path); + } + + /** + * Get stored cookies for a URL. + * + * Finds matching stored cookies and returns a simple array + * of name => value + * + * @param string $url The URL to find cookies for. + * @return array + */ + public function get($url) + { + $path = parse_url($url, PHP_URL_PATH) ?: '/'; + $host = parse_url($url, PHP_URL_HOST); + $scheme = parse_url($url, PHP_URL_SCHEME); + + return $this->findMatchingCookies($scheme, $host, $path); + } + + /** + * Get all the stored cookies as arrays. + * + * @return array + */ + public function getAll() + { + $out = []; + foreach ($this->cookies as $cookie) { + $out[] = $this->convertCookieToArray($cookie); + } + + return $out; + } + + /** + * Convert the cookie into an array of its properties. + * + * Primarily useful where backwards compatibility is needed. + * + * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie object. + * @return array + */ + protected function convertCookieToArray(CookieInterface $cookie) + { + return [ + 'name' => $cookie->getName(), + 'value' => $cookie->getValue(), + 'path' => $cookie->getPath(), + 'domain' => $cookie->getDomain(), + 'secure' => $cookie->isSecure(), + 'httponly' => $cookie->isHttpOnly(), + 'expires' => $cookie->getExpiresTimestamp() + ]; + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Http\Client\CookieCollection', 'Cake\Network\Http\CookieCollection'); diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/FormData.php b/app/vendor/cakephp/cakephp/src/Http/Client/FormData.php new file mode 100644 index 000000000..5e7a32ab0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Client/FormData.php @@ -0,0 +1,267 @@ +_boundary) { + return $this->_boundary; + } + $this->_boundary = md5(uniqid(time())); + + return $this->_boundary; + } + + /** + * Method for creating new instances of Part + * + * @param string $name The name of the part. + * @param string $value The value to add. + * @return \Cake\Http\Client\FormDataPart + */ + public function newPart($name, $value) + { + return new FormDataPart($name, $value); + } + + /** + * Add a new part to the data. + * + * The value for a part can be a string, array, int, + * float, filehandle, or object implementing __toString() + * + * If the $value is an array, multiple parts will be added. + * Files will be read from their current position and saved in memory. + * + * @param string|\Cake\Http\Client\FormData $name The name of the part to add, + * or the part data object. + * @param mixed $value The value for the part. + * @return $this + */ + public function add($name, $value = null) + { + if (is_array($value)) { + $this->addRecursive($name, $value); + } elseif (is_resource($value)) { + $this->addFile($name, $value); + } elseif ($name instanceof FormDataPart && $value === null) { + $this->_hasComplexPart = true; + $this->_parts[] = $name; + } else { + $this->_parts[] = $this->newPart($name, $value); + } + + return $this; + } + + /** + * Add multiple parts at once. + * + * Iterates the parameter and adds all the key/values. + * + * @param array $data Array of data to add. + * @return $this + */ + public function addMany(array $data) + { + foreach ($data as $name => $value) { + $this->add($name, $value); + } + + return $this; + } + + /** + * Add either a file reference (string starting with @) + * or a file handle. + * + * @param string $name The name to use. + * @param mixed $value Either a string filename, or a filehandle. + * @return \Cake\Http\Client\FormDataPart + */ + public function addFile($name, $value) + { + $this->_hasFile = true; + + $filename = false; + $contentType = 'application/octet-stream'; + if (is_resource($value)) { + $content = stream_get_contents($value); + if (stream_is_local($value)) { + $finfo = new finfo(FILEINFO_MIME); + $metadata = stream_get_meta_data($value); + $contentType = $finfo->file($metadata['uri']); + $filename = basename($metadata['uri']); + } + } else { + $finfo = new finfo(FILEINFO_MIME); + $value = substr($value, 1); + $filename = basename($value); + $content = file_get_contents($value); + $contentType = $finfo->file($value); + } + $part = $this->newPart($name, $content); + $part->type($contentType); + if ($filename) { + $part->filename($filename); + } + $this->add($part); + + return $part; + } + + /** + * Recursively add data. + * + * @param string $name The name to use. + * @param mixed $value The value to add. + * @return void + */ + public function addRecursive($name, $value) + { + foreach ($value as $key => $value) { + $key = $name . '[' . $key . ']'; + $this->add($key, $value); + } + } + + /** + * Returns the count of parts inside this object. + * + * @return int + */ + public function count() + { + return count($this->_parts); + } + + /** + * Check whether or not the current payload + * has any files. + * + * @return bool Whether or not there is a file in this payload. + */ + public function hasFile() + { + return $this->_hasFile; + } + + /** + * Check whether or not the current payload + * is multipart. + * + * A payload will become multipart when you add files + * or use add() with a Part instance. + * + * @return bool Whether or not the payload is multipart. + */ + public function isMultipart() + { + return $this->hasFile() || $this->_hasComplexPart; + } + + /** + * Get the content type for this payload. + * + * If this object contains files, `multipart/form-data` will be used, + * otherwise `application/x-www-form-urlencoded` will be used. + * + * @return string + */ + public function contentType() + { + if (!$this->isMultipart()) { + return 'application/x-www-form-urlencoded'; + } + + return 'multipart/form-data; boundary="' . $this->boundary() . '"'; + } + + /** + * Converts the FormData and its parts into a string suitable + * for use in an HTTP request. + * + * @return string + */ + public function __toString() + { + if ($this->isMultipart()) { + $boundary = $this->boundary(); + $out = ''; + foreach ($this->_parts as $part) { + $out .= "--$boundary\r\n"; + $out .= (string)$part; + $out .= "\r\n"; + } + $out .= "--$boundary--\r\n\r\n"; + + return $out; + } + $data = []; + foreach ($this->_parts as $part) { + $data[$part->name()] = $part->value(); + } + + return http_build_query($data); + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Http\Client\FormData', 'Cake\Network\Http\FormData'); diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/FormDataPart.php b/app/vendor/cakephp/cakephp/src/Http/Client/FormDataPart.php new file mode 100644 index 000000000..a079c7274 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Client/FormDataPart.php @@ -0,0 +1,226 @@ +_name = $name; + $this->_value = $value; + $this->_disposition = $disposition; + } + + /** + * Get/set the disposition type + * + * By passing in `false` you can disable the disposition + * header from being added. + * + * @param null|string $disposition Use null to get/string to set. + * @return string|null + */ + public function disposition($disposition = null) + { + if ($disposition === null) { + return $this->_disposition; + } + $this->_disposition = $disposition; + } + + /** + * Get/set the contentId for a part. + * + * @param null|string $id The content id. + * @return string|null + */ + public function contentId($id = null) + { + if ($id === null) { + return $this->_contentId; + } + $this->_contentId = $id; + } + + /** + * Get/set the filename. + * + * Setting the filename to `false` will exclude it from the + * generated output. + * + * @param null|string $filename Use null to get/string to set. + * @return string|null + */ + public function filename($filename = null) + { + if ($filename === null) { + return $this->_filename; + } + $this->_filename = $filename; + } + + /** + * Get/set the content type. + * + * @param null|string $type Use null to get/string to set. + * @return string|null + */ + public function type($type) + { + if ($type === null) { + return $this->_type; + } + $this->_type = $type; + } + + /** + * Set the transfer-encoding for multipart. + * + * Useful when content bodies are in encodings like base64. + * + * @param null|string $type The type of encoding the value has. + * @return string|null + */ + public function transferEncoding($type) + { + if ($type === null) { + return $this->_transferEncoding; + } + $this->_transferEncoding = $type; + } + + /** + * Get the part name. + * + * @return string + */ + public function name() + { + return $this->_name; + } + + /** + * Get the value. + * + * @return string + */ + public function value() + { + return $this->_value; + } + + /** + * Convert the part into a string. + * + * Creates a string suitable for use in HTTP requests. + * + * @return string + */ + public function __toString() + { + $out = ''; + if ($this->_disposition) { + $out .= 'Content-Disposition: ' . $this->_disposition; + if ($this->_name) { + $out .= '; name="' . $this->_name . '"'; + } + if ($this->_filename) { + $out .= '; filename="' . $this->_filename . '"'; + } + $out .= "\r\n"; + } + if ($this->_type) { + $out .= 'Content-Type: ' . $this->_type . "\r\n"; + } + if ($this->_transferEncoding) { + $out .= 'Content-Transfer-Encoding: ' . $this->_transferEncoding . "\r\n"; + } + if ($this->_contentId) { + $out .= 'Content-ID: <' . $this->_contentId . ">\r\n"; + } + $out .= "\r\n"; + $out .= (string)$this->_value; + + return $out; + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Http\Client\FormDataPart', 'Cake\Network\Http\FormData\Part'); diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Message.php b/app/vendor/cakephp/cakephp/src/Http/Client/Message.php new file mode 100644 index 000000000..eb11e2d87 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Client/Message.php @@ -0,0 +1,204 @@ +headers; + } + + /** + * Get all cookies + * + * @return array + */ + public function cookies() + { + return $this->_cookies; + } + + /** + * Get/set the body for the message. + * + * @param string|null $body The body for the request. Leave null for get + * @return mixed Either $this or the body value. + */ + public function body($body = null) + { + if ($body === null) { + return $this->_body; + } + $this->_body = $body; + + return $this; + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Http\Client\Message', 'Cake\Network\Http\Message'); diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Request.php b/app/vendor/cakephp/cakephp/src/Http/Client/Request.php new file mode 100644 index 000000000..7829a9ffc --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Client/Request.php @@ -0,0 +1,287 @@ +validateMethod($method); + $this->method = $method; + $this->uri = $this->createUri($url); + $headers += [ + 'Connection' => 'close', + 'User-Agent' => 'CakePHP' + ]; + $this->addHeaders($headers); + $this->body($data); + } + + /** + * Get/Set the HTTP method. + * + * *Warning* This method mutates the request in-place for backwards + * compatibility reasons, and is not part of the PSR7 interface. + * + * @param string|null $method The method for the request. + * @return $this|string Either this or the current method. + * @throws \Cake\Core\Exception\Exception On invalid methods. + * @deprecated 3.3.0 Use getMethod() and withMethod() instead. + */ + public function method($method = null) + { + deprecationWarning( + 'Request::method() is deprecated. ' . + 'Use getMethod() and withMethod() instead.' + ); + + if ($method === null) { + return $this->method; + } + $name = get_called_class() . '::METHOD_' . strtoupper($method); + if (!defined($name)) { + throw new Exception('Invalid method type'); + } + $this->method = $method; + + return $this; + } + + /** + * Get/Set the url for the request. + * + * *Warning* This method mutates the request in-place for backwards + * compatibility reasons, and is not part of the PSR7 interface. + * + * @param string|null $url The url for the request. Leave null for get + * @return $this|string Either $this or the url value. + * @deprecated 3.3.0 Use getUri() and withUri() instead. + */ + public function url($url = null) + { + deprecationWarning( + 'Request::url() is deprecated. ' . + 'Use getUri() and withUri() instead.' + ); + + if ($url === null) { + return '' . $this->getUri(); + } + $this->uri = $this->createUri($url); + + return $this; + } + + /** + * Get/Set headers into the request. + * + * You can get the value of a header, or set one/many headers. + * Headers are set / fetched in a case insensitive way. + * + * ### Getting headers + * + * ``` + * $request->header('Content-Type'); + * ``` + * + * ### Setting one header + * + * ``` + * $request->header('Content-Type', 'application/json'); + * ``` + * + * ### Setting multiple headers + * + * ``` + * $request->header(['Connection' => 'close', 'User-Agent' => 'CakePHP']); + * ``` + * + * *Warning* This method mutates the request in-place for backwards + * compatibility reasons, and is not part of the PSR7 interface. + * + * @param string|array|null $name The name to get, or array of multiple values to set. + * @param string|null $value The value to set for the header. + * @return mixed Either $this when setting or header value when getting. + * @deprecated 3.3.0 Use withHeader() and getHeaderLine() instead. + */ + public function header($name = null, $value = null) + { + deprecationWarning( + 'Request::header() is deprecated. ' . + 'Use withHeader() and getHeaderLine() instead.' + ); + + if ($value === null && is_string($name)) { + $val = $this->getHeaderLine($name); + if ($val === '') { + return null; + } + + return $val; + } + + if ($value !== null && !is_array($name)) { + $name = [$name => $value]; + } + $this->addHeaders($name); + + return $this; + } + + /** + * Add an array of headers to the request. + * + * @param array $headers The headers to add. + * @return void + */ + protected function addHeaders(array $headers) + { + foreach ($headers as $key => $val) { + $normalized = strtolower($key); + $this->headers[$key] = (array)$val; + $this->headerNames[$normalized] = $key; + } + } + + /** + * Get/Set cookie values. + * + * ### Getting a cookie + * + * ``` + * $request->cookie('session'); + * ``` + * + * ### Setting one cookie + * + * ``` + * $request->cookie('session', '123456'); + * ``` + * + * ### Setting multiple headers + * + * ``` + * $request->cookie(['test' => 'value', 'split' => 'banana']); + * ``` + * + * @param string $name The name of the cookie to get/set + * @param string|null $value Either the value or null when getting values. + * @return mixed Either $this or the cookie value. + * @deprecated 3.5.0 No longer used. CookieCollections now add `Cookie` header to the request + * before sending. Use Cake\Http\Cookie\CookieCollection::addToRequest() to make adding cookies + * to a request easier. + */ + public function cookie($name, $value = null) + { + deprecationWarning( + 'Request::cookie() is deprecated. ' . + 'The Client internals now add the required `Cookie` header to the ' . + 'request before sending. Use Cake\Http\Cookie\CookieCollection::addToRequest() ' . + 'to make adding cookies to a request easier.' + ); + + if ($value === null && is_string($name)) { + return isset($this->_cookies[$name]) ? $this->_cookies[$name] : null; + } + if (is_string($name) && is_string($value)) { + $name = [$name => $value]; + } + foreach ($name as $key => $val) { + $this->_cookies[$key] = $val; + } + + return $this; + } + + /** + * Get/Set HTTP version. + * + * *Warning* This method mutates the request in-place for backwards + * compatibility reasons, and is not part of the PSR7 interface. + * + * @param string|null $version The HTTP version. + * @return $this|string Either $this or the HTTP version. + * @deprecated 3.3.0 Use getProtocolVersion() and withProtocolVersion() instead. + */ + public function version($version = null) + { + deprecationWarning( + 'Request::version() is deprecated. ' . + 'Use getProtocolVersion() and withProtocolVersion() instead.' + ); + + if ($version === null) { + return $this->protocol; + } + + $this->protocol = $version; + + return $this; + } + + /** + * Get/set the body/payload for the message. + * + * Array data will be serialized with Cake\Http\FormData, + * and the content-type will be set. + * + * @param string|array|null $body The body for the request. Leave null for get + * @return mixed Either $this or the body value. + */ + public function body($body = null) + { + if ($body === null) { + $body = $this->getBody(); + + return $body ? $body->__toString() : ''; + } + if (is_array($body)) { + $formData = new FormData(); + $formData->addMany($body); + $this->addHeaders(['Content-Type' => $formData->contentType()]); + $body = (string)$formData; + } + $stream = new Stream('php://memory', 'rw'); + $stream->write($body); + $this->stream = $stream; + + return $this; + } +} + +// @deprecated Add backwards compact alias. +class_alias('Cake\Http\Client\Request', 'Cake\Network\Http\Request'); diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Response.php b/app/vendor/cakephp/cakephp/src/Http/Client/Response.php new file mode 100644 index 000000000..9e896c39b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Client/Response.php @@ -0,0 +1,673 @@ +getHeaderLine('content-type'); + * ``` + * + * Will read the Content-Type header. You can get all set + * headers using: + * + * ``` + * $response->getHeaders(); + * ``` + * + * You can also get at the headers using object access. When getting + * headers with object access, you have to use case-sensitive header + * names: + * + * ``` + * $val = $response->headers['Content-Type']; + * ``` + * + * ### Get the response body + * + * You can access the response body stream using: + * + * ``` + * $content = $response->getBody(); + * ``` + * + * You can also use object access to get the string version + * of the response body: + * + * ``` + * $content = $response->body; + * ``` + * + * If your response body is in XML or JSON you can use + * special content type specific accessors to read the decoded data. + * JSON data will be returned as arrays, while XML data will be returned + * as SimpleXML nodes: + * + * ``` + * // Get as xml + * $content = $response->xml + * // Get as json + * $content = $response->json + * ``` + * + * If the response cannot be decoded, null will be returned. + * + * ### Check the status code + * + * You can access the response status code using: + * + * ``` + * $content = $response->getStatusCode(); + * ``` + * + * You can also use object access: + * + * ``` + * $content = $response->code; + * ``` + */ +class Response extends Message implements ResponseInterface +{ + use MessageTrait; + + /** + * The status code of the response. + * + * @var int + */ + protected $code; + + /** + * Cookie Collection instance + * + * @var \Cake\Http\Cookie\CookieCollection + */ + protected $cookies; + + /** + * The reason phrase for the status code + * + * @var string + */ + protected $reasonPhrase; + + /** + * Cached decoded XML data. + * + * @var \SimpleXMLElement + */ + protected $_xml; + + /** + * Cached decoded JSON data. + * + * @var array + */ + protected $_json; + + /** + * Map of public => property names for __get() + * + * @var array + */ + protected $_exposedProperties = [ + 'cookies' => '_getCookies', + 'body' => '_getBody', + 'code' => 'code', + 'json' => '_getJson', + 'xml' => '_getXml', + 'headers' => '_getHeaders', + ]; + + /** + * Constructor + * + * @param array $headers Unparsed headers. + * @param string $body The response body. + */ + public function __construct($headers = [], $body = '') + { + $this->_parseHeaders($headers); + if ($this->getHeaderLine('Content-Encoding') === 'gzip') { + $body = $this->_decodeGzipBody($body); + } + $stream = new Stream('php://memory', 'wb+'); + $stream->write($body); + $stream->rewind(); + $this->stream = $stream; + } + + /** + * Uncompress a gzip response. + * + * Looks for gzip signatures, and if gzinflate() exists, + * the body will be decompressed. + * + * @param string $body Gzip encoded body. + * @return string + * @throws \RuntimeException When attempting to decode gzip content without gzinflate. + */ + protected function _decodeGzipBody($body) + { + if (!function_exists('gzinflate')) { + throw new RuntimeException('Cannot decompress gzip response body without gzinflate()'); + } + $offset = 0; + // Look for gzip 'signature' + if (substr($body, 0, 2) === "\x1f\x8b") { + $offset = 2; + } + // Check the format byte + if (substr($body, $offset, 1) === "\x08") { + return gzinflate(substr($body, $offset + 8)); + } + } + + /** + * Parses headers if necessary. + * + * - Decodes the status code and reasonphrase. + * - Parses and normalizes header names + values. + * + * @param array $headers Headers to parse. + * @return void + */ + protected function _parseHeaders($headers) + { + foreach ($headers as $key => $value) { + if (substr($value, 0, 5) === 'HTTP/') { + preg_match('/HTTP\/([\d.]+) ([0-9]+)(.*)/i', $value, $matches); + $this->protocol = $matches[1]; + $this->code = (int)$matches[2]; + $this->reasonPhrase = trim($matches[3]); + continue; + } + list($name, $value) = explode(':', $value, 2); + $value = trim($value); + $name = trim($name); + + $normalized = strtolower($name); + + if (isset($this->headers[$name])) { + $this->headers[$name][] = $value; + } else { + $this->headers[$name] = (array)$value; + $this->headerNames[$normalized] = $name; + } + } + } + + /** + * Check if the response was OK + * + * @return bool + */ + public function isOk() + { + $codes = [ + static::STATUS_OK, + static::STATUS_CREATED, + static::STATUS_ACCEPTED, + static::STATUS_NON_AUTHORITATIVE_INFORMATION, + static::STATUS_NO_CONTENT + ]; + + return in_array($this->code, $codes); + } + + /** + * Check if the response had a redirect status code. + * + * @return bool + */ + public function isRedirect() + { + $codes = [ + static::STATUS_MOVED_PERMANENTLY, + static::STATUS_FOUND, + static::STATUS_SEE_OTHER, + static::STATUS_TEMPORARY_REDIRECT, + ]; + + return ( + in_array($this->code, $codes) && + $this->getHeaderLine('Location') + ); + } + + /** + * Get the status code from the response + * + * @return int + * @deprecated 3.3.0 Use getStatusCode() instead. + */ + public function statusCode() + { + deprecationWarning( + 'Response::statusCode() is deprecated. ' . + 'Use Response::getStatusCode() instead.' + ); + + return $this->code; + } + + /** + * {@inheritdoc} + * + * @return int The status code. + */ + public function getStatusCode() + { + return $this->code; + } + + /** + * {@inheritdoc} + * + * @param int $code The status code to set. + * @param string $reasonPhrase The status reason phrase. + * @return $this A copy of the current object with an updated status code. + */ + public function withStatus($code, $reasonPhrase = '') + { + $new = clone $this; + $new->code = $code; + $new->reasonPhrase = $reasonPhrase; + + return $new; + } + + /** + * {@inheritdoc} + * + * @return string The current reason phrase. + */ + public function getReasonPhrase() + { + return $this->reasonPhrase; + } + + /** + * Get the encoding if it was set. + * + * @return string|null + * @deprecated 3.3.0 Use getEncoding() instead. + */ + public function encoding() + { + deprecationWarning( + 'Response::encoding() is deprecated. ' . + 'Use Response::getEncoding() instead.' + ); + + return $this->getEncoding(); + } + + /** + * Get the encoding if it was set. + * + * @return string|null + */ + public function getEncoding() + { + $content = $this->getHeaderLine('content-type'); + if (!$content) { + return null; + } + preg_match('/charset\s?=\s?[\'"]?([a-z0-9-_]+)[\'"]?/i', $content, $matches); + if (empty($matches[1])) { + return null; + } + + return $matches[1]; + } + + /** + * Read single/multiple header value(s) out. + * + * @param string|null $name The name of the header you want. Leave + * null to get all headers. + * @return mixed Null when the header doesn't exist. An array + * will be returned when getting all headers or when getting + * a header that had multiple values set. Otherwise a string + * will be returned. + * @deprecated 3.3.0 Use getHeader() and getHeaderLine() instead. + */ + public function header($name = null) + { + deprecationWarning( + 'Response::header() is deprecated. ' . + 'Use Response::getHeader() and getHeaderLine() instead.' + ); + + if ($name === null) { + return $this->_getHeaders(); + } + $header = $this->getHeader($name); + if (count($header) === 1) { + return $header[0]; + } + + return $header; + } + + /** + * Read single/multiple cookie values out. + * + * *Note* This method will only provide access to cookies that + * were added as part of the constructor. If cookies are added post + * construction they will not be accessible via this method. + * + * @param string|null $name The name of the cookie you want. Leave + * null to get all cookies. + * @param bool $all Get all parts of the cookie. When false only + * the value will be returned. + * @return mixed + * @deprecated 3.3.0 Use getCookie(), getCookieData() or getCookies() instead. + */ + public function cookie($name = null, $all = false) + { + deprecationWarning( + 'Response::cookie() is deprecated. ' . + 'Use Response::getCookie(), getCookieData() or getCookies() instead.' + ); + + if ($name === null) { + return $this->getCookies(); + } + if ($all) { + return $this->getCookieData($name); + } + + return $this->getCookie($name); + } + + /** + * Get the all cookie data. + * + * @return array The cookie data + */ + public function getCookies() + { + return $this->_getCookies(); + } + + /** + * Get the cookie collection from this response. + * + * This method exposes the response's CookieCollection + * instance allowing you to interact with cookie objects directly. + * + * @return \Cake\Http\Cookie\CookieCollection + */ + public function getCookieCollection() + { + $this->buildCookieCollection(); + + return $this->cookies; + } + + /** + * Get the value of a single cookie. + * + * @param string $name The name of the cookie value. + * @return string|array|null Either the cookie's value or null when the cookie is undefined. + */ + public function getCookie($name) + { + $this->buildCookieCollection(); + if (!$this->cookies->has($name)) { + return null; + } + + return $this->cookies->get($name)->getValue(); + } + + /** + * Get the full data for a single cookie. + * + * @param string $name The name of the cookie value. + * @return array|null Either the cookie's data or null when the cookie is undefined. + */ + public function getCookieData($name) + { + $this->buildCookieCollection(); + + if (!$this->cookies->has($name)) { + return null; + } + + $cookie = $this->cookies->get($name); + + return $this->convertCookieToArray($cookie); + } + + /** + * Convert the cookie into an array of its properties. + * + * This method is compatible with older client code that + * expects date strings instead of timestamps. + * + * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie object. + * @return array + */ + protected function convertCookieToArray(CookieInterface $cookie) + { + return [ + 'name' => $cookie->getName(), + 'value' => $cookie->getValue(), + 'path' => $cookie->getPath(), + 'domain' => $cookie->getDomain(), + 'secure' => $cookie->isSecure(), + 'httponly' => $cookie->isHttpOnly(), + 'expires' => $cookie->getFormattedExpires() + ]; + } + + /** + * Lazily build the CookieCollection and cookie objects from the response header + * + * @return void + */ + protected function buildCookieCollection() + { + if ($this->cookies) { + return; + } + $this->cookies = CookiesCollection::createFromHeader($this->getHeader('Set-Cookie')); + } + + /** + * Property accessor for `$this->cookies` + * + * @return array Array of Cookie data. + */ + protected function _getCookies() + { + $this->buildCookieCollection(); + + $cookies = []; + foreach ($this->cookies as $cookie) { + $cookies[$cookie->getName()] = $this->convertCookieToArray($cookie); + } + + return $cookies; + } + + /** + * Get the HTTP version used. + * + * @return string + * @deprecated 3.3.0 Use getProtocolVersion() + */ + public function version() + { + deprecationWarning( + 'Response::version() is deprecated. ' . + 'Use Response::getProtocolVersion() instead.' + ); + + return $this->protocol; + } + + /** + * Get the response body. + * + * By passing in a $parser callable, you can get the decoded + * response content back. + * + * For example to get the json data as an object: + * + * ``` + * $body = $response->body('json_decode'); + * ``` + * + * @param callable|null $parser The callback to use to decode + * the response body. + * @return mixed The response body. + */ + public function body($parser = null) + { + $stream = $this->stream; + $stream->rewind(); + if ($parser) { + return $parser($stream->getContents()); + } + + return $stream->getContents(); + } + + /** + * Get the response body as JSON decoded data. + * + * @return array|null + */ + protected function _getJson() + { + if ($this->_json) { + return $this->_json; + } + + return $this->_json = json_decode($this->_getBody(), true); + } + + /** + * Get the response body as XML decoded data. + * + * @return null|\SimpleXMLElement + */ + protected function _getXml() + { + if ($this->_xml) { + return $this->_xml; + } + libxml_use_internal_errors(); + $data = simplexml_load_string($this->_getBody()); + if ($data) { + $this->_xml = $data; + + return $this->_xml; + } + + return null; + } + + /** + * Provides magic __get() support. + * + * @return array + */ + protected function _getHeaders() + { + $out = []; + foreach ($this->headers as $key => $values) { + $out[$key] = implode(',', $values); + } + + return $out; + } + + /** + * Provides magic __get() support. + * + * @return array + */ + protected function _getBody() + { + $this->stream->rewind(); + + return $this->stream->getContents(); + } + + /** + * Read values as properties. + * + * @param string $name Property name. + * @return mixed + */ + public function __get($name) + { + if (!isset($this->_exposedProperties[$name])) { + return false; + } + $key = $this->_exposedProperties[$name]; + if (substr($key, 0, 4) === '_get') { + return $this->{$key}(); + } + + return $this->{$key}; + } + + /** + * isset/empty test with -> syntax. + * + * @param string $name Property name. + * @return bool + */ + public function __isset($name) + { + if (!isset($this->_exposedProperties[$name])) { + return false; + } + $key = $this->_exposedProperties[$name]; + if (substr($key, 0, 4) === '_get') { + $val = $this->{$key}(); + + return $val !== null; + } + + return isset($this->{$key}); + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Http\Client\Response', 'Cake\Network\Http\Response'); diff --git a/app/vendor/cakephp/cakephp/src/Http/ControllerFactory.php b/app/vendor/cakephp/cakephp/src/Http/ControllerFactory.php new file mode 100644 index 000000000..b68e9ee10 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/ControllerFactory.php @@ -0,0 +1,108 @@ +getControllerClass($request); + if (!$className) { + $this->missingController($request); + } + $reflection = new ReflectionClass($className); + if ($reflection->isAbstract() || $reflection->isInterface()) { + $this->missingController($request); + } + + return $reflection->newInstance($request, $response); + } + + /** + * Determine the controller class name based on current request and controller param + * + * @param \Cake\Http\ServerRequest $request The request to build a controller for. + * @return string|null + */ + public function getControllerClass(ServerRequest $request) + { + $pluginPath = $controller = null; + $namespace = 'Controller'; + if ($request->getParam('controller')) { + $controller = $request->getParam('controller'); + } + if ($request->getParam('plugin')) { + $pluginPath = $request->getParam('plugin') . '.'; + } + if ($request->getParam('prefix')) { + if (strpos($request->getParam('prefix'), '/') === false) { + $namespace .= '/' . Inflector::camelize($request->getParam('prefix')); + } else { + $prefixes = array_map( + 'Cake\Utility\Inflector::camelize', + explode('/', $request->getParam('prefix')) + ); + $namespace .= '/' . implode('/', $prefixes); + } + } + $firstChar = substr($controller, 0, 1); + + // Disallow plugin short forms, / and \\ from + // controller names as they allow direct references to + // be created. + if (strpos($controller, '\\') !== false || + strpos($controller, '/') !== false || + strpos($controller, '.') !== false || + $firstChar === strtolower($firstChar) + ) { + $this->missingController($request); + } + + return App::className($pluginPath . $controller, $namespace, 'Controller') ?: null; + } + + /** + * Throws an exception when a controller is missing. + * + * @param \Cake\Http\ServerRequest $request The request. + * @throws \Cake\Routing\Exception\MissingControllerException + * @return void + */ + protected function missingController($request) + { + throw new MissingControllerException([ + 'class' => $request->getParam('controller'), + 'plugin' => $request->getParam('plugin'), + 'prefix' => $request->getParam('prefix'), + '_ext' => $request->getParam('_ext') + ]); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Cookie/Cookie.php b/app/vendor/cakephp/cakephp/src/Http/Cookie/Cookie.php new file mode 100644 index 000000000..db524d16e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Cookie/Cookie.php @@ -0,0 +1,594 @@ +withValue('0'); + * ``` + * + * @link https://tools.ietf.org/html/rfc6265 + * @link https://en.wikipedia.org/wiki/HTTP_cookie + * @see Cake\Http\Cookie\CookieCollection for working with collections of cookies. + * @see Cake\Http\Response::getCookieCollection() for working with response cookies. + */ +class Cookie implements CookieInterface +{ + + /** + * Cookie name + * + * @var string + */ + protected $name = ''; + + /** + * Raw Cookie value. + * + * @var string|array + */ + protected $value = ''; + + /** + * Whether or not a JSON value has been expanded into an array. + * + * @var bool + */ + protected $isExpanded = false; + + /** + * Expiration time + * + * @var \DateTime|\DateTimeImmutable|null + */ + protected $expiresAt; + + /** + * Path + * + * @var string + */ + protected $path = ''; + + /** + * Domain + * + * @var string + */ + protected $domain = ''; + + /** + * Secure + * + * @var bool + */ + protected $secure = false; + + /** + * HTTP only + * + * @var bool + */ + protected $httpOnly = false; + + /** + * Constructor + * + * The constructors args are similar to the native PHP `setcookie()` method. + * The only difference is the 3rd argument which excepts null or an + * DateTime or DateTimeImmutable object instead an integer. + * + * @link http://php.net/manual/en/function.setcookie.php + * @param string $name Cookie name + * @param string|array $value Value of the cookie + * @param \DateTime|\DateTimeImmutable|null $expiresAt Expiration time and date + * @param string $path Path + * @param string $domain Domain + * @param bool $secure Is secure + * @param bool $httpOnly HTTP Only + */ + public function __construct( + $name, + $value = '', + $expiresAt = null, + $path = '', + $domain = '', + $secure = false, + $httpOnly = false + ) { + $this->validateName($name); + $this->name = $name; + + $this->_setValue($value); + + $this->validateString($domain); + $this->domain = $domain; + + $this->validateBool($httpOnly); + $this->httpOnly = $httpOnly; + + $this->validateString($path); + $this->path = $path; + + $this->validateBool($secure); + $this->secure = $secure; + if ($expiresAt) { + $expiresAt = $expiresAt->setTimezone(new DateTimeZone('GMT')); + } + $this->expiresAt = $expiresAt; + } + + /** + * Returns a header value as string + * + * @return string + */ + public function toHeaderValue() + { + $value = $this->value; + if ($this->isExpanded) { + $value = $this->_flatten($this->value); + } + $headerValue[] = sprintf('%s=%s', $this->name, rawurlencode($value)); + + if ($this->expiresAt) { + $headerValue[] = sprintf('expires=%s', $this->getFormattedExpires()); + } + if ($this->path !== '') { + $headerValue[] = sprintf('path=%s', $this->path); + } + if ($this->domain !== '') { + $headerValue[] = sprintf('domain=%s', $this->domain); + } + if ($this->secure) { + $headerValue[] = 'secure'; + } + if ($this->httpOnly) { + $headerValue[] = 'httponly'; + } + + return implode('; ', $headerValue); + } + + /** + * {@inheritDoc} + */ + public function withName($name) + { + $this->validateName($name); + $new = clone $this; + $new->name = $name; + + return $new; + } + + /** + * {@inheritDoc} + */ + public function getId() + { + return "{$this->name};{$this->domain};{$this->path}"; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * Validates the cookie name + * + * @param string $name Name of the cookie + * @return void + * @throws \InvalidArgumentException + * @link https://tools.ietf.org/html/rfc2616#section-2.2 Rules for naming cookies. + */ + protected function validateName($name) + { + if (preg_match("/[=,;\t\r\n\013\014]/", $name)) { + throw new InvalidArgumentException( + sprintf('The cookie name `%s` contains invalid characters.', $name) + ); + } + + if (empty($name)) { + throw new InvalidArgumentException('The cookie name cannot be empty.'); + } + } + + /** + * {@inheritDoc} + */ + public function getValue() + { + return $this->value; + } + + /** + * {@inheritDoc} + */ + public function getStringValue() + { + if ($this->isExpanded) { + return $this->_flatten($this->value); + } + + return $this->value; + } + + /** + * {@inheritDoc} + */ + public function withValue($value) + { + $new = clone $this; + $new->_setValue($value); + + return $new; + } + + /** + * Setter for the value attribute. + * + * @param mixed $value The value to store. + * @return void + */ + protected function _setValue($value) + { + $this->isExpanded = is_array($value); + $this->value = $value; + } + + /** + * {@inheritDoc} + */ + public function withPath($path) + { + $this->validateString($path); + $new = clone $this; + $new->path = $path; + + return $new; + } + + /** + * {@inheritDoc} + */ + public function getPath() + { + return $this->path; + } + + /** + * {@inheritDoc} + */ + public function withDomain($domain) + { + $this->validateString($domain); + $new = clone $this; + $new->domain = $domain; + + return $new; + } + + /** + * {@inheritDoc} + */ + public function getDomain() + { + return $this->domain; + } + + /** + * Validate that an argument is a string + * + * @param string $value The value to validate. + * @return void + * @throws \InvalidArgumentException + */ + protected function validateString($value) + { + if (!is_string($value)) { + throw new InvalidArgumentException(sprintf( + 'The provided arg must be of type `string` but `%s` given', + gettype($value) + )); + } + } + + /** + * {@inheritDoc} + */ + public function isSecure() + { + return $this->secure; + } + + /** + * {@inheritDoc} + */ + public function withSecure($secure) + { + $this->validateBool($secure); + $new = clone $this; + $new->secure = $secure; + + return $new; + } + + /** + * {@inheritDoc} + */ + public function withHttpOnly($httpOnly) + { + $this->validateBool($httpOnly); + $new = clone $this; + $new->httpOnly = $httpOnly; + + return $new; + } + + /** + * Validate that an argument is a boolean + * + * @param bool $value The value to validate. + * @return void + * @throws \InvalidArgumentException + */ + protected function validateBool($value) + { + if (!is_bool($value)) { + throw new InvalidArgumentException(sprintf( + 'The provided arg must be of type `bool` but `%s` given', + gettype($value) + )); + } + } + + /** + * {@inheritDoc} + */ + public function isHttpOnly() + { + return $this->httpOnly; + } + + /** + * {@inheritDoc} + */ + public function withExpiry($dateTime) + { + $new = clone $this; + $new->expiresAt = $dateTime->setTimezone(new DateTimeZone('GMT')); + + return $new; + } + + /** + * {@inheritDoc} + */ + public function getExpiry() + { + return $this->expiresAt; + } + + /** + * {@inheritDoc} + */ + public function getExpiresTimestamp() + { + if (!$this->expiresAt) { + return null; + } + + return $this->expiresAt->format('U'); + } + + /** + * {@inheritDoc} + */ + public function getFormattedExpires() + { + if (!$this->expiresAt) { + return ''; + } + + return $this->expiresAt->format(static::EXPIRES_FORMAT); + } + + /** + * {@inheritDoc} + */ + public function isExpired($time = null) + { + $time = $time ?: new DateTimeImmutable('now', new DateTimeZone('UTC')); + if (!$this->expiresAt) { + return false; + } + + return $this->expiresAt < $time; + } + + /** + * {@inheritDoc} + */ + public function withNeverExpire() + { + $new = clone $this; + $new->expiresAt = Chronos::createFromDate(2038, 1, 1); + + return $new; + } + + /** + * {@inheritDoc} + */ + public function withExpired() + { + $new = clone $this; + $new->expiresAt = Chronos::createFromTimestamp(1); + + return $new; + } + + /** + * Checks if a value exists in the cookie data. + * + * This method will expand serialized complex data, + * on first use. + * + * @param string $path Path to check + * @return bool + */ + public function check($path) + { + if ($this->isExpanded === false) { + $this->value = $this->_expand($this->value); + } + + return Hash::check($this->value, $path); + } + + /** + * Create a new cookie with updated data. + * + * @param string $path Path to write to + * @param mixed $value Value to write + * @return static + */ + public function withAddedValue($path, $value) + { + $new = clone $this; + if ($new->isExpanded === false) { + $new->value = $new->_expand($new->value); + } + $new->value = Hash::insert($new->value, $path, $value); + + return $new; + } + + /** + * Create a new cookie without a specific path + * + * @param string $path Path to remove + * @return static + */ + public function withoutAddedValue($path) + { + $new = clone $this; + if ($new->isExpanded === false) { + $new->value = $new->_expand($new->value); + } + $new->value = Hash::remove($new->value, $path); + + return $new; + } + + /** + * Read data from the cookie + * + * This method will expand serialized complex data, + * on first use. + * + * @param string $path Path to read the data from + * @return mixed + */ + public function read($path = null) + { + if ($this->isExpanded === false) { + $this->value = $this->_expand($this->value); + } + + if ($path === null) { + return $this->value; + } + + return Hash::get($this->value, $path); + } + + /** + * Checks if the cookie value was expanded + * + * @return bool + */ + public function isExpanded() + { + return $this->isExpanded; + } + + /** + * Implode method to keep keys are multidimensional arrays + * + * @param array $array Map of key and values + * @return string A json encoded string. + */ + protected function _flatten(array $array) + { + return json_encode($array); + } + + /** + * Explode method to return array from string set in CookieComponent::_flatten() + * Maintains reading backwards compatibility with 1.x CookieComponent::_flatten(). + * + * @param string $string A string containing JSON encoded data, or a bare string. + * @return string|array Map of key and values + */ + protected function _expand($string) + { + $this->isExpanded = true; + $first = substr($string, 0, 1); + if ($first === '{' || $first === '[') { + $ret = json_decode($string, true); + + return ($ret !== null) ? $ret : $string; + } + + $array = []; + foreach (explode(',', $string) as $pair) { + $key = explode('|', $pair); + if (!isset($key[1])) { + return $key[0]; + } + $array[$key[0]] = $key[1]; + } + + return $array; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieCollection.php b/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieCollection.php new file mode 100644 index 000000000..1e91778f8 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieCollection.php @@ -0,0 +1,423 @@ +checkCookies($cookies); + foreach ($cookies as $cookie) { + $this->cookies[$cookie->getId()] = $cookie; + } + } + + /** + * Create a Cookie Collection from an array of Set-Cookie Headers + * + * @param array $header The array of set-cookie header values. + * @return static + */ + public static function createFromHeader(array $header) + { + $cookies = static::parseSetCookieHeader($header); + + return new static($cookies); + } + + /** + * Create a new collection from the cookies in a ServerRequest + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request to extract cookie data from + * @return static + */ + public static function createFromServerRequest(ServerRequestInterface $request) + { + $data = $request->getCookieParams(); + $cookies = []; + foreach ($data as $name => $value) { + $cookies[] = new Cookie($name, $value); + } + + return new static($cookies); + } + + /** + * Get the number of cookies in the collection. + * + * @return int + */ + public function count() + { + return count($this->cookies); + } + + /** + * Add a cookie and get an updated collection. + * + * Cookies are stored by id. This means that there can be duplicate + * cookies if a cookie collection is used for cookies across multiple + * domains. This can impact how get(), has() and remove() behave. + * + * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie instance to add. + * @return static + */ + public function add(CookieInterface $cookie) + { + $new = clone $this; + $new->cookies[$cookie->getId()] = $cookie; + + return $new; + } + + /** + * Get the first cookie by name. + * + * @param string $name The name of the cookie. + * @return \Cake\Http\Cookie\CookieInterface|null + */ + public function get($name) + { + $key = mb_strtolower($name); + foreach ($this->cookies as $cookie) { + if (mb_strtolower($cookie->getName()) === $key) { + return $cookie; + } + } + + return null; + } + + /** + * Check if a cookie with the given name exists + * + * @param string $name The cookie name to check. + * @return bool True if the cookie exists, otherwise false. + */ + public function has($name) + { + $key = mb_strtolower($name); + foreach ($this->cookies as $cookie) { + if (mb_strtolower($cookie->getName()) === $key) { + return true; + } + } + + return false; + } + + /** + * Create a new collection with all cookies matching $name removed. + * + * If the cookie is not in the collection, this method will do nothing. + * + * @param string $name The name of the cookie to remove. + * @return static + */ + public function remove($name) + { + $new = clone $this; + $key = mb_strtolower($name); + foreach ($new->cookies as $i => $cookie) { + if (mb_strtolower($cookie->getName()) === $key) { + unset($new->cookies[$i]); + } + } + + return $new; + } + + /** + * Checks if only valid cookie objects are in the array + * + * @param array $cookies Array of cookie objects + * @return void + * @throws \InvalidArgumentException + */ + protected function checkCookies(array $cookies) + { + foreach ($cookies as $index => $cookie) { + if (!$cookie instanceof CookieInterface) { + throw new InvalidArgumentException( + sprintf( + 'Expected `%s[]` as $cookies but instead got `%s` at index %d', + static::class, + getTypeName($cookie), + $index + ) + ); + } + } + } + + /** + * Gets the iterator + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->cookies); + } + + /** + * Add cookies that match the path/domain/expiration to the request. + * + * This allows CookieCollections to be used as a 'cookie jar' in an HTTP client + * situation. Cookies that match the request's domain + path that are not expired + * when this method is called will be applied to the request. + * + * @param \Psr\Http\Message\RequestInterface $request The request to update. + * @param array $extraCookies Associative array of additional cookies to add into the request. This + * is useful when you have cookie data from outside the collection you want to send. + * @return \Psr\Http\Message\RequestInterface An updated request. + */ + public function addToRequest(RequestInterface $request, array $extraCookies = []) + { + $uri = $request->getUri(); + $cookies = $this->findMatchingCookies( + $uri->getScheme(), + $uri->getHost(), + $uri->getPath() ?: '/' + ); + $cookies = array_merge($cookies, $extraCookies); + $cookiePairs = []; + foreach ($cookies as $key => $value) { + $cookie = sprintf("%s=%s", rawurlencode($key), rawurlencode($value)); + $size = strlen($cookie); + if ($size > 4096) { + triggerWarning(sprintf( + 'The cookie `%s` exceeds the recommended maximum cookie length of 4096 bytes.', + $key + )); + } + $cookiePairs[] = $cookie; + } + + if (empty($cookiePairs)) { + return $request; + } + + return $request->withHeader('Cookie', implode('; ', $cookiePairs)); + } + + /** + * Find cookies matching the scheme, host, and path + * + * @param string $scheme The http scheme to match + * @param string $host The host to match. + * @param string $path The path to match + * @return array An array of cookie name/value pairs + */ + protected function findMatchingCookies($scheme, $host, $path) + { + $out = []; + $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); + foreach ($this->cookies as $cookie) { + if ($scheme === 'http' && $cookie->isSecure()) { + continue; + } + if (strpos($path, $cookie->getPath()) !== 0) { + continue; + } + $domain = $cookie->getDomain(); + $leadingDot = substr($domain, 0, 1) === '.'; + if ($leadingDot) { + $domain = ltrim($domain, '.'); + } + + if ($cookie->isExpired($now)) { + continue; + } + + $pattern = '/' . preg_quote($domain, '/') . '$/'; + if (!preg_match($pattern, $host)) { + continue; + } + + $out[$cookie->getName()] = $cookie->getValue(); + } + + return $out; + } + + /** + * Create a new collection that includes cookies from the response. + * + * @param \Psr\Http\Message\ResponseInterface $response Response to extract cookies from. + * @param \Psr\Http\Message\RequestInterface $request Request to get cookie context from. + * @return static + */ + public function addFromResponse(ResponseInterface $response, RequestInterface $request) + { + $uri = $request->getUri(); + $host = $uri->getHost(); + $path = $uri->getPath() ?: '/'; + + $cookies = static::parseSetCookieHeader($response->getHeader('Set-Cookie')); + $cookies = $this->setRequestDefaults($cookies, $host, $path); + $new = clone $this; + foreach ($cookies as $cookie) { + $new->cookies[$cookie->getId()] = $cookie; + } + $new->removeExpiredCookies($host, $path); + + return $new; + } + + /** + * Apply path and host to the set of cookies if they are not set. + * + * @param array $cookies An array of cookies to update. + * @param string $host The host to set. + * @param string $path The path to set. + * @return array An array of updated cookies. + */ + protected function setRequestDefaults(array $cookies, $host, $path) + { + $out = []; + foreach ($cookies as $name => $cookie) { + if (!$cookie->getDomain()) { + $cookie = $cookie->withDomain($host); + } + if (!$cookie->getPath()) { + $cookie = $cookie->withPath($path); + } + $out[] = $cookie; + } + + return $out; + } + + /** + * Parse Set-Cookie headers into array + * + * @param array $values List of Set-Cookie Header values. + * @return \Cake\Http\Cookie\Cookie[] An array of cookie objects + */ + protected static function parseSetCookieHeader($values) + { + $cookies = []; + foreach ($values as $value) { + $value = rtrim($value, ';'); + $parts = preg_split('/\;[ \t]*/', $value); + + $name = false; + $cookie = [ + 'value' => '', + 'path' => '', + 'domain' => '', + 'secure' => false, + 'httponly' => false, + 'expires' => null, + 'max-age' => null + ]; + foreach ($parts as $i => $part) { + if (strpos($part, '=') !== false) { + list($key, $value) = explode('=', $part, 2); + } else { + $key = $part; + $value = true; + } + if ($i === 0) { + $name = $key; + $cookie['value'] = urldecode($value); + continue; + } + $key = strtolower($key); + if (array_key_exists($key, $cookie) && !strlen($cookie[$key])) { + $cookie[$key] = $value; + } + } + try { + $expires = null; + if ($cookie['max-age'] !== null) { + $expires = new DateTimeImmutable('@' . (time() + $cookie['max-age'])); + } elseif ($cookie['expires']) { + $expires = new DateTimeImmutable('@' . strtotime($cookie['expires'])); + } + } catch (Exception $e) { + $expires = null; + } + + try { + $cookies[] = new Cookie( + $name, + $cookie['value'], + $expires, + $cookie['path'], + $cookie['domain'], + $cookie['secure'], + $cookie['httponly'] + ); + } catch (Exception $e) { + // Don't blow up on invalid cookies + } + } + + return $cookies; + } + + /** + * Remove expired cookies from the collection. + * + * @param string $host The host to check for expired cookies on. + * @param string $path The path to check for expired cookies on. + * @return void + */ + protected function removeExpiredCookies($host, $path) + { + $time = new DateTimeImmutable('now', new DateTimeZone('UTC')); + $hostPattern = '/' . preg_quote($host, '/') . '$/'; + + foreach ($this->cookies as $i => $cookie) { + $expired = $cookie->isExpired($time); + $pathMatches = strpos($path, $cookie->getPath()) === 0; + $hostMatches = preg_match($hostPattern, $cookie->getDomain()); + if ($pathMatches && $hostMatches && $expired) { + unset($this->cookies[$i]); + } + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieInterface.php b/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieInterface.php new file mode 100644 index 000000000..3d752812f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieInterface.php @@ -0,0 +1,201 @@ +_origin = $origin; + $this->_isSsl = $isSsl; + $this->_response = $response; + } + + /** + * Apply the queued headers to the response. + * + * If the builder has no Origin, or if there are no allowed domains, + * or if the allowed domains do not match the Origin header no headers will be applied. + * + * @return \Psr\Http\Message\MessageInterface A new instance of the response with new headers. + */ + public function build() + { + $response = $this->_response; + if (empty($this->_origin)) { + return $response; + } + + if (isset($this->_headers['Access-Control-Allow-Origin'])) { + foreach ($this->_headers as $key => $value) { + $response = $response->withHeader($key, $value); + } + } + + return $response; + } + + /** + * Set the list of allowed domains. + * + * Accepts a string or an array of domains that have CORS enabled. + * You can use `*.example.com` wildcards to accept subdomains, or `*` to allow all domains + * + * @param string|array $domain The allowed domains + * @return $this + */ + public function allowOrigin($domain) + { + $allowed = $this->_normalizeDomains((array)$domain); + foreach ($allowed as $domain) { + if (!preg_match($domain['preg'], $this->_origin)) { + continue; + } + $value = $domain['original'] === '*' ? '*' : $this->_origin; + $this->_headers['Access-Control-Allow-Origin'] = $value; + break; + } + + return $this; + } + + /** + * Normalize the origin to regular expressions and put in an array format + * + * @param array $domains Domain names to normalize. + * @return array + */ + protected function _normalizeDomains($domains) + { + $result = []; + foreach ($domains as $domain) { + if ($domain === '*') { + $result[] = ['preg' => '@.@', 'original' => '*']; + continue; + } + + $original = $preg = $domain; + if (strpos($domain, '://') === false) { + $preg = ($this->_isSsl ? 'https://' : 'http://') . $domain; + } + $preg = '@^' . str_replace('\*', '.*', preg_quote($preg, '@')) . '$@'; + $result[] = compact('original', 'preg'); + } + + return $result; + } + + /** + * Set the list of allowed HTTP Methods. + * + * @param array $methods The allowed HTTP methods + * @return $this + */ + public function allowMethods(array $methods) + { + $this->_headers['Access-Control-Allow-Methods'] = implode(', ', $methods); + + return $this; + } + + /** + * Enable cookies to be sent in CORS requests. + * + * @return $this + */ + public function allowCredentials() + { + $this->_headers['Access-Control-Allow-Credentials'] = 'true'; + + return $this; + } + + /** + * Whitelist headers that can be sent in CORS requests. + * + * @param array $headers The list of headers to accept in CORS requests. + * @return $this + */ + public function allowHeaders(array $headers) + { + $this->_headers['Access-Control-Allow-Headers'] = implode(', ', $headers); + + return $this; + } + + /** + * Define the headers a client library/browser can expose to scripting + * + * @param array $headers The list of headers to expose CORS responses + * @return $this + */ + public function exposeHeaders(array $headers) + { + $this->_headers['Access-Control-Expose-Headers'] = implode(', ', $headers); + + return $this; + } + + /** + * Define the max-age preflight OPTIONS requests are valid for. + * + * @param int $age The max-age for OPTIONS requests in seconds + * @return $this + */ + public function maxAge($age) + { + $this->_headers['Access-Control-Max-Age'] = $age; + + return $this; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Exception/BadRequestException.php b/app/vendor/cakephp/cakephp/src/Http/Exception/BadRequestException.php new file mode 100644 index 000000000..ea51c0d8e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Exception/BadRequestException.php @@ -0,0 +1,40 @@ + true, 'xml' => false, 'methods' => null]; + if ($options['json']) { + $this->addParser( + ['application/json', 'text/json'], + [$this, 'decodeJson'] + ); + } + if ($options['xml']) { + $this->addParser( + ['application/xml', 'text/xml'], + [$this, 'decodeXml'] + ); + } + if ($options['methods']) { + $this->setMethods($options['methods']); + } + } + + /** + * Set the HTTP methods to parse request bodies on. + * + * @param array $methods The methods to parse data on. + * @return $this + */ + public function setMethods(array $methods) + { + $this->methods = $methods; + + return $this; + } + + /** + * Add a parser. + * + * Map a set of content-type header values to be parsed by the $parser. + * + * ### Example + * + * An naive CSV request body parser could be built like so: + * + * ``` + * $parser->addParser(['text/csv'], function ($body) { + * return str_getcsv($body); + * }); + * ``` + * + * @param array $types An array of content-type header values to match. eg. application/json + * @param callable $parser The parser function. Must return an array of data to be inserted + * into the request. + * @return $this + */ + public function addParser(array $types, callable $parser) + { + foreach ($types as $type) { + $type = strtolower($type); + $this->parsers[$type] = $parser; + } + + return $this; + } + + /** + * Apply the middleware. + * + * Will modify the request adding a parsed body if the content-type is known. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param \Psr\Http\Message\ResponseInterface $response The response. + * @param callable $next Callback to invoke the next middleware. + * @return \Cake\Http\Response A response + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) + { + if (!in_array($request->getMethod(), $this->methods)) { + return $next($request, $response); + } + list($type) = explode(';', $request->getHeaderLine('Content-Type')); + $type = strtolower($type); + if (!isset($this->parsers[$type])) { + return $next($request, $response); + } + + $parser = $this->parsers[$type]; + $result = $parser($request->getBody()->getContents()); + if (!is_array($result)) { + throw new BadRequestException(); + } + $request = $request->withParsedBody($result); + + return $next($request, $response); + } + + /** + * Decode JSON into an array. + * + * @param string $body The request body to decode + * @return array + */ + protected function decodeJson($body) + { + return json_decode($body, true); + } + + /** + * Decode XML into an array. + * + * @param string $body The request body to decode + * @return array + */ + protected function decodeXml($body) + { + try { + $xml = Xml::build($body, ['return' => 'domdocument', 'readFile' => false]); + // We might not get child nodes if there are nested inline entities. + if ($xml->childNodes->length > 0) { + return Xml::toArray($xml); + } + + return []; + } catch (XmlException $e) { + return []; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/CsrfProtectionMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/CsrfProtectionMiddleware.php new file mode 100644 index 000000000..8e0b15590 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/CsrfProtectionMiddleware.php @@ -0,0 +1,198 @@ +Form->create(...)` is used in a view. + */ +class CsrfProtectionMiddleware +{ + /** + * Default config for the CSRF handling. + * + * - `cookieName` The name of the cookie to send. + * - `expiry` A strotime compatible value of how long the CSRF token should last. + * Defaults to browser session. + * - `secure` Whether or not the cookie will be set with the Secure flag. Defaults to false. + * - `httpOnly` Whether or not the cookie will be set with the HttpOnly flag. Defaults to false. + * - `field` The form field to check. Changing this will also require configuring + * FormHelper. + * + * @var array + */ + protected $_defaultConfig = [ + 'cookieName' => 'csrfToken', + 'expiry' => 0, + 'secure' => false, + 'httpOnly' => false, + 'field' => '_csrfToken', + ]; + + /** + * Configuration + * + * @var array + */ + protected $_config = []; + + /** + * Constructor + * + * @param array $config Config options. See $_defaultConfig for valid keys. + */ + public function __construct(array $config = []) + { + $this->_config = $config + $this->_defaultConfig; + } + + /** + * Checks and sets the CSRF token depending on the HTTP verb. + * + * @param \Cake\Http\ServerRequest $request The request. + * @param \Cake\Http\Response $response The response. + * @param callable $next Callback to invoke the next middleware. + * @return \Cake\Http\Response A response + */ + public function __invoke(ServerRequest $request, Response $response, $next) + { + $cookies = $request->getCookieParams(); + $cookieData = Hash::get($cookies, $this->_config['cookieName']); + + if (strlen($cookieData) > 0) { + $params = $request->getAttribute('params'); + $params['_csrfToken'] = $cookieData; + $request = $request->withAttribute('params', $params); + } + + $method = $request->getMethod(); + if ($method === 'GET' && $cookieData === null) { + $token = $this->_createToken(); + $request = $this->_addTokenToRequest($token, $request); + $response = $this->_addTokenCookie($token, $request, $response); + + return $next($request, $response); + } + $request = $this->_validateAndUnsetTokenField($request); + + return $next($request, $response); + } + + /** + * Checks if the request is POST, PUT, DELETE or PATCH and validates the CSRF token + * + * @param \Cake\Http\ServerRequest $request The request object. + * @return \Cake\Http\ServerRequest + */ + protected function _validateAndUnsetTokenField(ServerRequest $request) + { + if (in_array($request->getMethod(), ['PUT', 'POST', 'DELETE', 'PATCH']) || $request->getData()) { + $this->_validateToken($request); + $body = $request->getParsedBody(); + if (is_array($body)) { + unset($body[$this->_config['field']]); + $request = $request->withParsedBody($body); + } + } + + return $request; + } + + /** + * Create a new token to be used for CSRF protection + * + * @return string + */ + protected function _createToken() + { + return hash('sha512', Security::randomBytes(16), false); + } + + /** + * Add a CSRF token to the request parameters. + * + * @param string $token The token to add. + * @param \Cake\Http\ServerRequest $request The request to augment + * @return \Cake\Http\ServerRequest Modified request + */ + protected function _addTokenToRequest($token, ServerRequest $request) + { + $params = $request->getAttribute('params'); + $params['_csrfToken'] = $token; + + return $request->withAttribute('params', $params); + } + + /** + * Add a CSRF token to the response cookies. + * + * @param string $token The token to add. + * @param \Cake\Http\ServerRequest $request The request to validate against. + * @param \Cake\Http\Response $response The response. + * @return \Cake\Http\Response $response Modified response. + */ + protected function _addTokenCookie($token, ServerRequest $request, Response $response) + { + $expiry = new Time($this->_config['expiry']); + + return $response->withCookie($this->_config['cookieName'], [ + 'value' => $token, + 'expire' => $expiry->format('U'), + 'path' => $request->getAttribute('webroot'), + 'secure' => $this->_config['secure'], + 'httpOnly' => $this->_config['httpOnly'], + ]); + } + + /** + * Validate the request data against the cookie token. + * + * @param \Cake\Http\ServerRequest $request The request to validate against. + * @return void + * @throws \Cake\Http\Exception\InvalidCsrfTokenException When the CSRF token is invalid or missing. + */ + protected function _validateToken(ServerRequest $request) + { + $cookies = $request->getCookieParams(); + $cookie = Hash::get($cookies, $this->_config['cookieName']); + $post = Hash::get($request->getParsedBody(), $this->_config['field']); + $header = $request->getHeaderLine('X-CSRF-Token'); + + if (!$cookie) { + throw new InvalidCsrfTokenException(__d('cake', 'Missing CSRF token cookie')); + } + + if (!Security::constantEquals($post, $cookie) && !Security::constantEquals($header, $cookie)) { + throw new InvalidCsrfTokenException(__d('cake', 'CSRF token mismatch.')); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/EncryptedCookieMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/EncryptedCookieMiddleware.php new file mode 100644 index 000000000..6555cfd25 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/EncryptedCookieMiddleware.php @@ -0,0 +1,169 @@ +cookieNames = $cookieNames; + $this->key = $key; + $this->cipherType = $cipherType; + } + + /** + * Apply cookie encryption/decryption. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param \Psr\Http\Message\ResponseInterface $response The response. + * @param callable $next The next middleware to call. + * @return \Psr\Http\Message\ResponseInterface A response. + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) + { + if ($request->getCookieParams()) { + $request = $this->decodeCookies($request); + } + $response = $next($request, $response); + if ($response->hasHeader('Set-Cookie')) { + $response = $this->encodeSetCookieHeader($response); + } + if ($response instanceof Response) { + $response = $this->encodeCookies($response); + } + + return $response; + } + + /** + * Fetch the cookie encryption key. + * + * Part of the CookieCryptTrait implementation. + * + * @return string + */ + protected function _getCookieEncryptionKey() + { + return $this->key; + } + + /** + * Decode cookies from the request. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request to decode cookies from. + * @return \Psr\Http\Message\ServerRequestInterface Updated request with decoded cookies. + */ + protected function decodeCookies(ServerRequestInterface $request) + { + $cookies = $request->getCookieParams(); + foreach ($this->cookieNames as $name) { + if (isset($cookies[$name])) { + $cookies[$name] = $this->_decrypt($cookies[$name], $this->cipherType, $this->key); + } + } + + return $request->withCookieParams($cookies); + } + + /** + * Encode cookies from a response's CookieCollection. + * + * @param \Cake\Http\Response $response The response to encode cookies in. + * @return \Cake\Http\Response Updated response with encoded cookies. + */ + protected function encodeCookies(Response $response) + { + $cookies = $response->getCookieCollection(); + foreach ($cookies as $cookie) { + if (in_array($cookie->getName(), $this->cookieNames, true)) { + $value = $this->_encrypt($cookie->getValue(), $this->cipherType); + $response = $response->withCookie($cookie->withValue($value)); + } + } + + return $response; + } + + /** + * Encode cookies from a response's Set-Cookie header + * + * @param \Psr\Http\Message\ResponseInterface $response The response to encode cookies in. + * @return \Psr\Http\Message\ResponseInterface Updated response with encoded cookies. + */ + protected function encodeSetCookieHeader(ResponseInterface $response) + { + $cookies = CookieCollection::createFromHeader($response->getHeader('Set-Cookie')); + $header = []; + foreach ($cookies as $cookie) { + if (in_array($cookie->getName(), $this->cookieNames, true)) { + $value = $this->_encrypt($cookie->getValue(), $this->cipherType); + $cookie = $cookie->withValue($value); + } + $header[] = $cookie->toHeaderValue(); + } + + return $response->withHeader('Set-Cookie', $header); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/SecurityHeadersMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/SecurityHeadersMiddleware.php new file mode 100644 index 000000000..2c29f2907 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/SecurityHeadersMiddleware.php @@ -0,0 +1,183 @@ +headers['x-content-type-options'] = 'nosniff'; + + return $this; + } + + /** + * X-Download-Options + * + * Sets the header value for it to 'noopen' + * + * @link https://msdn.microsoft.com/en-us/library/jj542450(v=vs.85).aspx + * @return $this + */ + public function noOpen() + { + $this->headers['x-download-options'] = 'noopen'; + + return $this; + } + + /** + * Referrer-Policy + * + * @link https://w3c.github.io/webappsec-referrer-policy + * @param string $policy Policy value. Available Value: 'no-referrer', 'no-referrer-when-downgrade', 'origin', 'origin-when-cross-origin', + * 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin', 'unsafe-url' + * @return $this + */ + public function setReferrerPolicy($policy = 'same-origin') + { + $available = [ + 'no-referrer', 'no-referrer-when-downgrade', 'origin', + 'origin-when-cross-origin', + 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin', + 'unsafe-url' + ]; + + $this->checkValues($policy, $available); + $this->headers['referrer-policy'] = $policy; + + return $this; + } + + /** + * X-Frame-Options + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + * @param string $option Option value. Available Values: 'deny', 'sameorigin', 'allow-from ' + * @param string $url URL if mode is `allow-from` + * @return $this + */ + public function setXFrameOptions($option = 'sameorigin', $url = null) + { + $this->checkValues($option, ['deny', 'sameorigin', 'allow-from']); + + if ($option === 'allow-from') { + if (empty($url)) { + throw new InvalidArgumentException('The 2nd arg $url can not be empty when `allow-from` is used'); + } + $option .= ' ' . $url; + } + + $this->headers['x-frame-options'] = $option; + + return $this; + } + + /** + * X-XSS-Protection + * + * @link https://blogs.msdn.microsoft.com/ieinternals/2011/01/31/controlling-the-xss-filter + * @param string $mode Mode value. Available Values: '1', '0', 'block' + * @return $this + */ + public function setXssProtection($mode = 'block') + { + $mode = (string)$mode; + + if ($mode === 'block') { + $mode = '1; mode=block'; + } + + $this->checkValues($mode, ['1', '0', '1; mode=block']); + $this->headers['x-xss-protection'] = $mode; + + return $this; + } + + /** + * X-Permitted-Cross-Domain-Policies + * + * @link https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html + * @param string $policy Policy value. Available Values: 'all', 'none', 'master-only', 'by-content-type', 'by-ftp-filename' + * @return $this + */ + public function setCrossDomainPolicy($policy = 'all') + { + $this->checkValues($policy, ['all', 'none', 'master-only', 'by-content-type', 'by-ftp-filename']); + $this->headers['x-permitted-cross-domain-policies'] = $policy; + + return $this; + } + + /** + * Convenience method to check if a value is in the list of allowed args + * + * @throws \InvalidArgumentException Thrown when a value is invalid. + * @param string $value Value to check + * @param array $allowed List of allowed values + * @return void + */ + protected function checkValues($value, array $allowed) + { + if (!in_array($value, $allowed)) { + throw new InvalidArgumentException(sprintf( + 'Invalid arg `%s`, use one of these: %s', + $value, + implode(', ', $allowed) + )); + } + } + + /** + * Serve assets if the path matches one. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param \Psr\Http\Message\ResponseInterface $response The response. + * @param callable $next Callback to invoke the next middleware. + * @return \Psr\Http\Message\ResponseInterface A response + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) + { + $response = $next($request, $response); + foreach ($this->headers as $header => $value) { + $response = $response->withHeader($header, $value); + } + + return $response; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/MiddlewareQueue.php b/app/vendor/cakephp/cakephp/src/Http/MiddlewareQueue.php new file mode 100644 index 000000000..7f12d3bb1 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/MiddlewareQueue.php @@ -0,0 +1,233 @@ +queue = $middleware; + } + + /** + * Get the middleware at the provided index. + * + * @param int $index The index to fetch. + * @return callable|null Either the callable middleware or null + * if the index is undefined. + */ + public function get($index) + { + if (isset($this->callables[$index])) { + return $this->callables[$index]; + } + + return $this->resolve($index); + } + + /** + * Resolve middleware name to callable. + * + * @param int $index The index to fetch. + * @return callable|null Either the callable middleware or null + * if the index is undefined. + */ + protected function resolve($index) + { + if (!isset($this->queue[$index])) { + return null; + } + + if (is_string($this->queue[$index])) { + $class = $this->queue[$index]; + $className = App::className($class, 'Middleware', 'Middleware'); + if (!$className || !class_exists($className)) { + throw new RuntimeException(sprintf( + 'Middleware "%s" was not found.', + $class + )); + } + $callable = new $className; + } else { + $callable = $this->queue[$index]; + } + + return $this->callables[$index] = $callable; + } + + /** + * Append a middleware callable to the end of the queue. + * + * @param callable|string|array $middleware The middleware(s) to append. + * @return $this + */ + public function add($middleware) + { + if (is_array($middleware)) { + $this->queue = array_merge($this->queue, $middleware); + + return $this; + } + $this->queue[] = $middleware; + + return $this; + } + + /** + * Alias for MiddlewareQueue::add(). + * + * @param callable|string|array $middleware The middleware(s) to append. + * @return $this + * @see MiddlewareQueue::add() + */ + public function push($middleware) + { + return $this->add($middleware); + } + + /** + * Prepend a middleware to the start of the queue. + * + * @param callable|string|array $middleware The middleware(s) to prepend. + * @return $this + */ + public function prepend($middleware) + { + if (is_array($middleware)) { + $this->queue = array_merge($middleware, $this->queue); + + return $this; + } + array_unshift($this->queue, $middleware); + + return $this; + } + + /** + * Insert a middleware callable at a specific index. + * + * If the index already exists, the new callable will be inserted, + * and the existing element will be shifted one index greater. + * + * @param int $index The index to insert at. + * @param callable|string $middleware The middleware to insert. + * @return $this + */ + public function insertAt($index, $middleware) + { + array_splice($this->queue, $index, 0, [$middleware]); + + return $this; + } + + /** + * Insert a middleware object before the first matching class. + * + * Finds the index of the first middleware that matches the provided class, + * and inserts the supplied callable before it. + * + * @param string $class The classname to insert the middleware before. + * @param callable|string $middleware The middleware to insert. + * @return $this + * @throws \LogicException If middleware to insert before is not found. + */ + public function insertBefore($class, $middleware) + { + $found = false; + $i = null; + foreach ($this->queue as $i => $object) { + if ((is_string($object) && $object === $class) + || is_a($object, $class) + ) { + $found = true; + break; + } + } + if ($found) { + return $this->insertAt($i, $middleware); + } + throw new LogicException(sprintf("No middleware matching '%s' could be found.", $class)); + } + + /** + * Insert a middleware object after the first matching class. + * + * Finds the index of the first middleware that matches the provided class, + * and inserts the supplied callable after it. If the class is not found, + * this method will behave like add(). + * + * @param string $class The classname to insert the middleware before. + * @param callable|string $middleware The middleware to insert. + * @return $this + */ + public function insertAfter($class, $middleware) + { + $found = false; + $i = null; + foreach ($this->queue as $i => $object) { + if ((is_string($object) && $object === $class) + || is_a($object, $class) + ) { + $found = true; + break; + } + } + if ($found) { + return $this->insertAt($i + 1, $middleware); + } + + return $this->add($middleware); + } + + /** + * Get the number of connected middleware layers. + * + * Implement the Countable interface. + * + * @return int + */ + public function count() + { + return count($this->queue); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/RequestTransformer.php b/app/vendor/cakephp/cakephp/src/Http/RequestTransformer.php new file mode 100644 index 000000000..df89d29a2 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/RequestTransformer.php @@ -0,0 +1,157 @@ +getParsedBody(); + $headers = []; + foreach ($request->getHeaders() as $k => $value) { + $name = sprintf('HTTP_%s', strtoupper(str_replace('-', '_', $k))); + $headers[$name] = implode(',', $value); + } + $server = $headers + $request->getServerParams(); + + $files = static::getFiles($request); + if (!empty($files)) { + $post = Hash::merge($post, $files); + } + + $input = $request->getBody()->getContents(); + $input = $input === '' ? null : $input; + + return new ServerRequest([ + 'query' => $request->getQueryParams(), + 'post' => $post, + 'cookies' => $request->getCookieParams(), + 'environment' => $server, + 'params' => static::getParams($request), + 'url' => $request->getUri()->getPath(), + 'base' => $request->getAttribute('base', ''), + 'webroot' => $request->getAttribute('webroot', '/'), + 'session' => $request->getAttribute('session', null), + 'input' => $input, + ]); + } + + /** + * Extract the routing parameters out of the request object. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request to extract params from. + * @return array The routing parameters. + */ + protected static function getParams(PsrRequest $request) + { + $params = (array)$request->getAttribute('params', []); + $params += [ + 'plugin' => null, + 'controller' => null, + 'action' => null, + '_ext' => null, + 'pass' => [] + ]; + + return $params; + } + + /** + * Extract the uploaded files out of the request object. + * + * CakePHP expects to get arrays of file information and + * not the parsed objects that PSR7 requests contain. Downsample the data here. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request to extract files from. + * @return array The routing parameters. + */ + protected static function getFiles($request) + { + return static::convertFiles([], $request->getUploadedFiles()); + } + + /** + * Convert a nested array of files to arrays. + * + * @param array $data The data to add files to. + * @param array $files The file objects to convert. + * @param string $path The current array path. + * @return array Converted file data + */ + protected static function convertFiles($data, $files, $path = '') + { + foreach ($files as $key => $file) { + $newPath = $path; + if ($newPath === '') { + $newPath = $key; + } + if ($newPath !== $key) { + $newPath .= '.' . $key; + } + + if (is_array($file)) { + $data = static::convertFiles($data, $file, $newPath); + } else { + $data = Hash::insert($data, $newPath, static::convertFile($file)); + } + } + + return $data; + } + + /** + * Convert a single file back into an array. + * + * @param \Psr\Http\Message\UploadedFileInterface $file The file to convert. + * @return array + */ + protected static function convertFile($file) + { + $error = $file->getError(); + $tmpName = ''; + if ($error === UPLOAD_ERR_OK) { + $tmpName = $file->getStream()->getMetadata('uri'); + } + + return [ + 'name' => $file->getClientFilename(), + 'type' => $file->getClientMediaType(), + 'tmp_name' => $tmpName, + 'error' => $error, + 'size' => $file->getSize(), + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Response.php b/app/vendor/cakephp/cakephp/src/Http/Response.php new file mode 100644 index 000000000..d698983e1 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Response.php @@ -0,0 +1,2820 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-status', + 208 => 'Already Reported', + 226 => 'IM used', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => '(Unused)', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Requested range not satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 421 => 'Misdirected Request', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 444 => 'Connection Closed Without Response', + 451 => 'Unavailable For Legal Reasons', + 499 => 'Client Closed Request', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'Unsupported Version', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', + 599 => 'Network Connect Timeout Error', + ]; + + /** + * Holds type key to mime type mappings for known mime types. + * + * @var array + */ + protected $_mimeTypes = [ + 'html' => ['text/html', '*/*'], + 'json' => 'application/json', + 'xml' => ['application/xml', 'text/xml'], + 'xhtml' => ['application/xhtml+xml', 'application/xhtml', 'text/xhtml'], + 'webp' => 'image/webp', + 'rss' => 'application/rss+xml', + 'ai' => 'application/postscript', + 'bcpio' => 'application/x-bcpio', + 'bin' => 'application/octet-stream', + 'ccad' => 'application/clariscad', + 'cdf' => 'application/x-netcdf', + 'class' => 'application/octet-stream', + 'cpio' => 'application/x-cpio', + 'cpt' => 'application/mac-compactpro', + 'csh' => 'application/x-csh', + 'csv' => ['text/csv', 'application/vnd.ms-excel'], + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dms' => 'application/octet-stream', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'drw' => 'application/drafting', + 'dvi' => 'application/x-dvi', + 'dwg' => 'application/acad', + 'dxf' => 'application/dxf', + 'dxr' => 'application/x-director', + 'eot' => 'application/vnd.ms-fontobject', + 'eps' => 'application/postscript', + 'exe' => 'application/octet-stream', + 'ez' => 'application/andrew-inset', + 'flv' => 'video/x-flv', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'bz2' => 'application/x-bzip', + '7z' => 'application/x-7z-compressed', + 'hdf' => 'application/x-hdf', + 'hqx' => 'application/mac-binhex40', + 'ico' => 'image/x-icon', + 'ips' => 'application/x-ipscript', + 'ipx' => 'application/x-ipix', + 'js' => 'application/javascript', + 'jsonapi' => 'application/vnd.api+json', + 'latex' => 'application/x-latex', + 'lha' => 'application/octet-stream', + 'lsp' => 'application/x-lisp', + 'lzh' => 'application/octet-stream', + 'man' => 'application/x-troff-man', + 'me' => 'application/x-troff-me', + 'mif' => 'application/vnd.mif', + 'ms' => 'application/x-troff-ms', + 'nc' => 'application/x-netcdf', + 'oda' => 'application/oda', + 'otf' => 'font/otf', + 'pdf' => 'application/pdf', + 'pgn' => 'application/x-chess-pgn', + 'pot' => 'application/vnd.ms-powerpoint', + 'pps' => 'application/vnd.ms-powerpoint', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ppz' => 'application/vnd.ms-powerpoint', + 'pre' => 'application/x-freelance', + 'prt' => 'application/pro_eng', + 'ps' => 'application/postscript', + 'roff' => 'application/x-troff', + 'scm' => 'application/x-lotusscreencam', + 'set' => 'application/set', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'sit' => 'application/x-stuffit', + 'skd' => 'application/x-koan', + 'skm' => 'application/x-koan', + 'skp' => 'application/x-koan', + 'skt' => 'application/x-koan', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'sol' => 'application/solids', + 'spl' => 'application/x-futuresplash', + 'src' => 'application/x-wais-source', + 'step' => 'application/STEP', + 'stl' => 'application/SLA', + 'stp' => 'application/STEP', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'swf' => 'application/x-shockwave-flash', + 't' => 'application/x-troff', + 'tar' => 'application/x-tar', + 'tcl' => 'application/x-tcl', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'tr' => 'application/x-troff', + 'tsp' => 'application/dsptype', + 'ttc' => 'font/ttf', + 'ttf' => 'font/ttf', + 'unv' => 'application/i-deas', + 'ustar' => 'application/x-ustar', + 'vcd' => 'application/x-cdlink', + 'vda' => 'application/vda', + 'xlc' => 'application/vnd.ms-excel', + 'xll' => 'application/vnd.ms-excel', + 'xlm' => 'application/vnd.ms-excel', + 'xls' => 'application/vnd.ms-excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xlw' => 'application/vnd.ms-excel', + 'zip' => 'application/zip', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'au' => 'audio/basic', + 'kar' => 'audio/midi', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mpga' => 'audio/mpeg', + 'ogg' => 'audio/ogg', + 'oga' => 'audio/ogg', + 'spx' => 'audio/ogg', + 'ra' => 'audio/x-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'snd' => 'audio/basic', + 'tsi' => 'audio/TSP-audio', + 'wav' => 'audio/x-wav', + 'aac' => 'audio/aac', + 'asc' => 'text/plain', + 'c' => 'text/plain', + 'cc' => 'text/plain', + 'css' => 'text/css', + 'etx' => 'text/x-setext', + 'f' => 'text/plain', + 'f90' => 'text/plain', + 'h' => 'text/plain', + 'hh' => 'text/plain', + 'htm' => ['text/html', '*/*'], + 'ics' => 'text/calendar', + 'm' => 'text/plain', + 'rtf' => 'text/rtf', + 'rtx' => 'text/richtext', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'tsv' => 'text/tab-separated-values', + 'tpl' => 'text/template', + 'txt' => 'text/plain', + 'text' => 'text/plain', + 'avi' => 'video/x-msvideo', + 'fli' => 'video/x-fli', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'qt' => 'video/quicktime', + 'viv' => 'video/vnd.vivo', + 'vivo' => 'video/vnd.vivo', + 'ogv' => 'video/ogg', + 'webm' => 'video/webm', + 'mp4' => 'video/mp4', + 'm4v' => 'video/mp4', + 'f4v' => 'video/mp4', + 'f4p' => 'video/mp4', + 'm4a' => 'audio/mp4', + 'f4a' => 'audio/mp4', + 'f4b' => 'audio/mp4', + 'gif' => 'image/gif', + 'ief' => 'image/ief', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'pbm' => 'image/x-portable-bitmap', + 'pgm' => 'image/x-portable-graymap', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'ppm' => 'image/x-portable-pixmap', + 'ras' => 'image/cmu-raster', + 'rgb' => 'image/x-rgb', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'xbm' => 'image/x-xbitmap', + 'xpm' => 'image/x-xpixmap', + 'xwd' => 'image/x-xwindowdump', + 'psd' => ['application/photoshop', 'application/psd', 'image/psd', 'image/x-photoshop', 'image/photoshop', 'zz-application/zz-winassoc-psd'], + 'ice' => 'x-conference/x-cooltalk', + 'iges' => 'model/iges', + 'igs' => 'model/iges', + 'mesh' => 'model/mesh', + 'msh' => 'model/mesh', + 'silo' => 'model/mesh', + 'vrml' => 'model/vrml', + 'wrl' => 'model/vrml', + 'mime' => 'www/mime', + 'pdb' => 'chemical/x-pdb', + 'xyz' => 'chemical/x-pdb', + 'javascript' => 'application/javascript', + 'form' => 'application/x-www-form-urlencoded', + 'file' => 'multipart/form-data', + 'xhtml-mobile' => 'application/vnd.wap.xhtml+xml', + 'atom' => 'application/atom+xml', + 'amf' => 'application/x-amf', + 'wap' => ['text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'], + 'wml' => 'text/vnd.wap.wml', + 'wmlscript' => 'text/vnd.wap.wmlscript', + 'wbmp' => 'image/vnd.wap.wbmp', + 'woff' => 'application/x-font-woff', + 'appcache' => 'text/cache-manifest', + 'manifest' => 'text/cache-manifest', + 'htc' => 'text/x-component', + 'rdf' => 'application/xml', + 'crx' => 'application/x-chrome-extension', + 'oex' => 'application/x-opera-extension', + 'xpi' => 'application/x-xpinstall', + 'safariextz' => 'application/octet-stream', + 'webapp' => 'application/x-web-app-manifest+json', + 'vcf' => 'text/x-vcard', + 'vtt' => 'text/vtt', + 'mkv' => 'video/x-matroska', + 'pkpass' => 'application/vnd.apple.pkpass', + 'ajax' => 'text/html', + 'bmp' => 'image/bmp' + ]; + + /** + * Protocol header to send to the client + * + * @var string + */ + protected $_protocol = 'HTTP/1.1'; + + /** + * Status code to send to the client + * + * @var int + */ + protected $_status = 200; + + /** + * Content type to send. This can be an 'extension' that will be transformed using the $_mimetypes array + * or a complete mime-type + * + * @var string + */ + protected $_contentType = 'text/html'; + + /** + * File object for file to be read out as response + * + * @var \Cake\Filesystem\File|null + */ + protected $_file; + + /** + * File range. Used for requesting ranges of files. + * + * @var array + */ + protected $_fileRange = []; + + /** + * The charset the response body is encoded with + * + * @var string + */ + protected $_charset = 'UTF-8'; + + /** + * Holds all the cache directives that will be converted + * into headers when sending the request + * + * @var array + */ + protected $_cacheDirectives = []; + + /** + * Collection of cookies to send to the client + * + * @var \Cake\Http\Cookie\CookieCollection + */ + protected $_cookies = null; + + /** + * Reason Phrase + * + * @var string + */ + protected $_reasonPhrase = 'OK'; + + /** + * Stream mode options. + * + * @var string + */ + protected $_streamMode = 'wb+'; + + /** + * Stream target or resource object. + * + * @var string|resource + */ + protected $_streamTarget = 'php://memory'; + + /** + * Constructor + * + * @param array $options list of parameters to setup the response. Possible values are: + * - body: the response text that should be sent to the client + * - statusCodes: additional allowable response codes + * - status: the HTTP status code to respond with + * - type: a complete mime-type string or an extension mapped in this class + * - charset: the charset for the response body + */ + public function __construct(array $options = []) + { + if (isset($options['streamTarget'])) { + $this->_streamTarget = $options['streamTarget']; + } + if (isset($options['streamMode'])) { + $this->_streamMode = $options['streamMode']; + } + if (isset($options['stream'])) { + if (!$options['stream'] instanceof StreamInterface) { + throw new InvalidArgumentException('Stream option must be an object that implements StreamInterface'); + } + $this->stream = $options['stream']; + } else { + $this->_createStream(); + } + if (isset($options['body'])) { + $this->stream->write($options['body']); + } + if (isset($options['statusCodes'])) { + $this->httpCodes($options['statusCodes']); + } + if (isset($options['status'])) { + $this->_setStatus($options['status']); + } + if (!isset($options['charset'])) { + $options['charset'] = Configure::read('App.encoding'); + } + $this->_charset = $options['charset']; + if (isset($options['type'])) { + $this->_contentType = $this->resolveType($options['type']); + } + $this->_setContentType(); + $this->_cookies = new CookieCollection(); + } + + /** + * Creates the stream object. + * + * @return void + */ + protected function _createStream() + { + $this->stream = new Stream($this->_streamTarget, $this->_streamMode); + } + + /** + * Sends the complete response to the client including headers and message body. + * Will echo out the content in the response body. + * + * @return void + * @deprecated 3.4.0 Will be removed in 4.0.0 + */ + public function send() + { + deprecationWarning('Response::send() will be removed in 4.0.0'); + + if ($this->hasHeader('Location') && $this->_status === 200) { + $this->statusCode(302); + } + + $this->_setContent(); + $this->sendHeaders(); + + if ($this->_file) { + $this->_sendFile($this->_file, $this->_fileRange); + $this->_file = null; + $this->_fileRange = []; + } else { + $this->_sendContent($this->body()); + } + + if (function_exists('fastcgi_finish_request')) { + fastcgi_finish_request(); + } + } + + /** + * Sends the HTTP headers and cookies. + * + * @return void + * @deprecated 3.4.0 Will be removed in 4.0.0 + */ + public function sendHeaders() + { + deprecationWarning( + 'Will be removed in 4.0.0' + ); + + $file = $line = null; + if (headers_sent($file, $line)) { + Log::warning("Headers already sent in {$file}:{$line}"); + + return; + } + + $codeMessage = $this->_statusCodes[$this->_status]; + $this->_setCookies(); + $this->_sendHeader("{$this->_protocol} {$this->_status} {$codeMessage}"); + $this->_setContentType(); + + foreach ($this->headers as $header => $values) { + foreach ((array)$values as $value) { + $this->_sendHeader($header, $value); + } + } + } + + /** + * Sets the cookies that have been added via Cake\Http\Response::cookie() before any + * other output is sent to the client. Will set the cookies in the order they + * have been set. + * + * @return void + * @deprecated 3.4.0 Will be removed in 4.0.0 + */ + protected function _setCookies() + { + deprecationWarning( + 'Will be removed in 4.0.0' + ); + + foreach ($this->_cookies as $cookie) { + setcookie( + $cookie->getName(), + $cookie->getValue(), + $cookie->getExpiresTimestamp(), + $cookie->getPath(), + $cookie->getDomain(), + $cookie->isSecure(), + $cookie->isHttpOnly() + ); + } + } + + /** + * Formats the Content-Type header based on the configured contentType and charset + * the charset will only be set in the header if the response is of type text/* + * + * @return void + */ + protected function _setContentType() + { + if (in_array($this->_status, [304, 204])) { + $this->_clearHeader('Content-Type'); + + return; + } + $whitelist = [ + 'application/javascript', 'application/json', 'application/xml', 'application/rss+xml' + ]; + + $charset = false; + if ($this->_charset && + (strpos($this->_contentType, 'text/') === 0 || in_array($this->_contentType, $whitelist)) + ) { + $charset = true; + } + + if ($charset) { + $this->_setHeader('Content-Type', "{$this->_contentType}; charset={$this->_charset}"); + } else { + $this->_setHeader('Content-Type', (string)$this->_contentType); + } + } + + /** + * Sets the response body to an empty text if the status code is 204 or 304 + * + * @return void + * @deprecated 3.4.0 Will be removed in 4.0.0 + */ + protected function _setContent() + { + deprecationWarning( + 'Will be removed in 4.0.0' + ); + + if (in_array($this->_status, [304, 204])) { + $this->body(''); + } + } + + /** + * Sends a header to the client. + * + * @param string $name the header name + * @param string|null $value the header value + * @return void + * @deprecated 3.4.0 Will be removed in 4.0.0 + */ + protected function _sendHeader($name, $value = null) + { + deprecationWarning( + 'Will be removed in 4.0.0' + ); + + if ($value === null) { + header($name); + } else { + header("{$name}: {$value}"); + } + } + + /** + * Sends a content string to the client. + * + * If the content is a callable, it is invoked. The callable should either + * return a string or output content directly and have no return value. + * + * @param string|callable $content String to send as response body or callable + * which returns/outputs content. + * @return void + * @deprecated 3.4.0 Will be removed in 4.0.0 + */ + protected function _sendContent($content) + { + deprecationWarning( + 'Will be removed in 4.0.0' + ); + + if (!is_string($content) && is_callable($content)) { + $content = $content(); + } + + echo $content; + } + + /** + * Buffers a header string to be sent + * Returns the complete list of buffered headers + * + * ### Single header + * ``` + * header('Location', 'http://example.com'); + * ``` + * + * ### Multiple headers + * ``` + * header(['Location' => 'http://example.com', 'X-Extra' => 'My header']); + * ``` + * + * ### String header + * ``` + * header('WWW-Authenticate: Negotiate'); + * ``` + * + * ### Array of string headers + * ``` + * header(['WWW-Authenticate: Negotiate', 'Content-type: application/pdf']); + * ``` + * + * Multiple calls for setting the same header name will have the same effect as setting the header once + * with the last value sent for it + * ``` + * header('WWW-Authenticate: Negotiate'); + * header('WWW-Authenticate: Not-Negotiate'); + * ``` + * will have the same effect as only doing + * ``` + * header('WWW-Authenticate: Not-Negotiate'); + * ``` + * + * @param string|array|null $header An array of header strings or a single header string + * - an associative array of "header name" => "header value" is also accepted + * - an array of string headers is also accepted + * @param string|array|null $value The header value(s) + * @return array List of headers to be sent + * @deprecated 3.4.0 Use `withHeader()`, `getHeaderLine()` and `getHeaders()` instead. + */ + public function header($header = null, $value = null) + { + deprecationWarning( + 'Response::header() is deprecated. ' . + 'Use `withHeader()`, `getHeaderLine()` and `getHeaders()` instead.' + ); + + if ($header === null) { + return $this->getSimpleHeaders(); + } + + $headers = is_array($header) ? $header : [$header => $value]; + foreach ($headers as $header => $value) { + if (is_numeric($header)) { + list($header, $value) = [$value, null]; + } + if ($value === null) { + list($header, $value) = explode(':', $header, 2); + } + + $lower = strtolower($header); + if (array_key_exists($lower, $this->headerNames)) { + $header = $this->headerNames[$lower]; + } else { + $this->headerNames[$lower] = $header; + } + + $this->headers[$header] = is_array($value) ? array_map('trim', $value) : [trim($value)]; + } + + return $this->getSimpleHeaders(); + } + + /** + * Backwards compatibility helper for getting flattened headers. + * + * Previously CakePHP would store headers as a simple dictionary, now that + * we're supporting PSR7, the internal storage has each header as an array. + * + * @return array + */ + protected function getSimpleHeaders() + { + $out = []; + foreach ($this->headers as $key => $values) { + $header = $this->headerNames[strtolower($key)]; + if (count($values) === 1) { + $values = $values[0]; + } + $out[$header] = $values; + } + + return $out; + } + + /** + * Accessor for the location header. + * + * Get/Set the Location header value. + * + * @param null|string $url Either null to get the current location, or a string to set one. + * @return string|null When setting the location null will be returned. When reading the location + * a string of the current location header value (if any) will be returned. + * @deprecated 3.4.0 Mutable responses are deprecated. Use `withLocation()` and `getHeaderLine()` + * instead. + */ + public function location($url = null) + { + deprecationWarning( + 'Response::location() is deprecated. ' . + 'Mutable responses are deprecated. Use `withLocation()` and `getHeaderLine()` instead.' + ); + + if ($url === null) { + $result = $this->getHeaderLine('Location'); + if (!$result) { + return null; + } + + return $result; + } + if ($this->_status === 200) { + $this->_status = 302; + } + $this->_setHeader('Location', $url); + + return null; + } + + /** + * Return an instance with an updated location header. + * + * If the current status code is 200, it will be replaced + * with 302. + * + * @param string $url The location to redirect to. + * @return static A new response with the Location header set. + */ + public function withLocation($url) + { + $new = $this->withHeader('Location', $url); + if ($new->_status === 200) { + $new->_status = 302; + } + + return $new; + } + + /** + * Sets a header. + * + * @param string $header Header key. + * @param string $value Header value. + * @return void + */ + protected function _setHeader($header, $value) + { + $normalized = strtolower($header); + $this->headerNames[$normalized] = $header; + $this->headers[$header] = [$value]; + } + + /** + * Clear header + * + * @param string $header Header key. + * @return void + */ + protected function _clearHeader($header) + { + $normalized = strtolower($header); + if (!isset($this->headerNames[$normalized])) { + return; + } + $original = $this->headerNames[$normalized]; + unset($this->headerNames[$normalized], $this->headers[$original]); + } + + /** + * Buffers the response message to be sent + * if $content is null the current buffer is returned + * + * @param string|callable|null $content the string or callable message to be sent + * @return string|null Current message buffer if $content param is passed as null + * @deprecated 3.4.0 Mutable response methods are deprecated. Use `withBody()`/`withStringBody()` and `getBody()` instead. + */ + public function body($content = null) + { + deprecationWarning( + 'Response::body() is deprecated. ' . + 'Mutable response methods are deprecated. Use `withBody()` and `getBody()` instead.' + ); + + if ($content === null) { + if ($this->stream->isSeekable()) { + $this->stream->rewind(); + } + $result = $this->stream->getContents(); + if (strlen($result) === 0) { + return null; + } + + return $result; + } + + // Compatibility with closure/streaming responses + if (!is_string($content) && is_callable($content)) { + $this->stream = new CallbackStream($content); + } else { + $this->_createStream(); + $this->stream->write($content); + } + + return $content; + } + + /** + * Handles the callable body for backward compatibility reasons. + * + * @param callable $content Callable content. + * @return string + */ + protected function _handleCallableBody(callable $content) + { + ob_start(); + $result1 = $content(); + $result2 = ob_get_contents(); + ob_get_clean(); + + if ($result1) { + return $result1; + } + + return $result2; + } + + /** + * Sets the HTTP status code to be sent + * if $code is null the current code is returned + * + * If the status code is 304 or 204, the existing Content-Type header + * will be cleared, as these response codes have no body. + * + * @param int|null $code the HTTP status code + * @return int Current status code + * @throws \InvalidArgumentException When an unknown status code is reached. + * @deprecated 3.4.0 Use `getStatusCode()` and `withStatus()` instead. + */ + public function statusCode($code = null) + { + deprecationWarning( + 'Response::statusCode() is deprecated. ' . + 'Use `getStatusCode()` and `withStatus()` instead.' + ); + + if ($code === null) { + return $this->_status; + } + if (!isset($this->_statusCodes[$code])) { + throw new InvalidArgumentException('Unknown status code'); + } + $this->_setStatus($code); + + return $code; + } + + /** + * Gets the response status code. + * + * The status code is a 3-digit integer result code of the server's attempt + * to understand and satisfy the request. + * + * @return int Status code. + */ + public function getStatusCode() + { + return $this->_status; + } + + /** + * Return an instance with the specified status code and, optionally, reason phrase. + * + * If no reason phrase is specified, implementations MAY choose to default + * to the RFC 7231 or IANA recommended reason phrase for the response's + * status code. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated status and reason phrase. + * + * If the status code is 304 or 204, the existing Content-Type header + * will be cleared, as these response codes have no body. + * + * @link https://tools.ietf.org/html/rfc7231#section-6 + * @link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @param int $code The 3-digit integer result code to set. + * @param string $reasonPhrase The reason phrase to use with the + * provided status code; if none is provided, implementations MAY + * use the defaults as suggested in the HTTP specification. + * @return static + * @throws \InvalidArgumentException For invalid status code arguments. + */ + public function withStatus($code, $reasonPhrase = '') + { + $new = clone $this; + $new->_setStatus($code, $reasonPhrase); + + return $new; + } + + /** + * Modifier for response status + * + * @param int $code The code to set. + * @param string $reasonPhrase The response reason phrase. + * @return void + * @throws \InvalidArgumentException For invalid status code arguments. + */ + protected function _setStatus($code, $reasonPhrase = '') + { + if (!isset($this->_statusCodes[$code])) { + throw new InvalidArgumentException(sprintf( + 'Invalid status code: %s. Use a valid HTTP status code in range 1xx - 5xx.', + $code + )); + } + + $this->_status = $code; + if (empty($reasonPhrase)) { + $reasonPhrase = $this->_statusCodes[$code]; + } + $this->_reasonPhrase = $reasonPhrase; + $this->_setContentType(); + } + + /** + * Gets the response reason phrase associated with the status code. + * + * Because a reason phrase is not a required element in a response + * status line, the reason phrase value MAY be null. Implementations MAY + * choose to return the default RFC 7231 recommended reason phrase (or those + * listed in the IANA HTTP Status Code Registry) for the response's + * status code. + * + * @link https://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @return string Reason phrase; must return an empty string if none present. + */ + public function getReasonPhrase() + { + return $this->_reasonPhrase; + } + + /** + * Queries & sets valid HTTP response codes & messages. + * + * @param int|array|null $code If $code is an integer, then the corresponding code/message is + * returned if it exists, null if it does not exist. If $code is an array, then the + * keys are used as codes and the values as messages to add to the default HTTP + * codes. The codes must be integers greater than 99 and less than 1000. Keep in + * mind that the HTTP specification outlines that status codes begin with a digit + * between 1 and 5, which defines the class of response the client is to expect. + * Example: + * + * httpCodes(404); // returns [404 => 'Not Found'] + * + * httpCodes([ + * 381 => 'Unicorn Moved', + * 555 => 'Unexpected Minotaur' + * ]); // sets these new values, and returns true + * + * httpCodes([ + * 0 => 'Nothing Here', + * -1 => 'Reverse Infinity', + * 12345 => 'Universal Password', + * 'Hello' => 'World' + * ]); // throws an exception due to invalid codes + * + * For more on HTTP status codes see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1 + * + * @return mixed Associative array of the HTTP codes as keys, and the message + * strings as values, or null of the given $code does not exist. + * @throws \InvalidArgumentException If an attempt is made to add an invalid status code + * @deprecated 3.4.0 Will be removed in 4.0.0 + */ + public function httpCodes($code = null) + { + deprecationWarning('Response::httpCodes(). Will be removed in 4.0.0'); + + if (empty($code)) { + return $this->_statusCodes; + } + if (is_array($code)) { + $codes = array_keys($code); + $min = min($codes); + if (!is_int($min) || $min < 100 || max($codes) > 999) { + throw new InvalidArgumentException('Invalid status code'); + } + $this->_statusCodes = $code + $this->_statusCodes; + + return true; + } + if (!isset($this->_statusCodes[$code])) { + return null; + } + + return [$code => $this->_statusCodes[$code]]; + } + + /** + * Sets the response content type. It can be either a file extension + * which will be mapped internally to a mime-type or a string representing a mime-type + * if $contentType is null the current content type is returned + * if $contentType is an associative array, content type definitions will be stored/replaced + * + * ### Setting the content type + * + * ``` + * type('jpg'); + * ``` + * + * If you attempt to set the type on a 304 or 204 status code response, the + * content type will not take effect as these status codes do not have content-types. + * + * ### Returning the current content type + * + * ``` + * type(); + * ``` + * + * ### Storing content type definitions + * + * ``` + * type(['keynote' => 'application/keynote', 'bat' => 'application/bat']); + * ``` + * + * ### Replacing a content type definition + * + * ``` + * type(['jpg' => 'text/plain']); + * ``` + * + * @param string|null $contentType Content type key. + * @return mixed Current content type or false if supplied an invalid content type. + * @deprecated 3.5.5 Use getType() or withType() instead. + */ + public function type($contentType = null) + { + deprecationWarning( + 'Response::type() is deprecated. ' . + 'Use getType() or withType() instead.' + ); + + if ($contentType === null) { + return $this->getType(); + } + if (is_array($contentType)) { + foreach ($contentType as $type => $definition) { + $this->_mimeTypes[$type] = $definition; + } + + return $this->getType(); + } + if (isset($this->_mimeTypes[$contentType])) { + $contentType = $this->_mimeTypes[$contentType]; + $contentType = is_array($contentType) ? current($contentType) : $contentType; + } + if (strpos($contentType, '/') === false) { + return false; + } + $this->_contentType = $contentType; + $this->_setContentType(); + + return $contentType; + } + + /** + * Returns the current content type. + * + * @return string + */ + public function getType() + { + return $this->_contentType; + } + + /** + * Get an updated response with the content type set. + * + * If you attempt to set the type on a 304 or 204 status code response, the + * content type will not take effect as these status codes do not have content-types. + * + * @param string $contentType Either a file extension which will be mapped to a mime-type or a concrete mime-type. + * @return static + */ + public function withType($contentType) + { + $mappedType = $this->resolveType($contentType); + $new = clone $this; + $new->_contentType = $mappedType; + $new->_setContentType(); + + return $new; + } + + /** + * Translate and validate content-types. + * + * @param string $contentType The content-type or type alias. + * @return string The resolved content-type + * @throws \InvalidArgumentException When an invalid content-type or alias is used. + */ + protected function resolveType($contentType) + { + $mapped = $this->getMimeType($contentType); + if ($mapped) { + return is_array($mapped) ? current($mapped) : $mapped; + } + if (strpos($contentType, '/') === false) { + throw new InvalidArgumentException(sprintf('"%s" is an invalid content type.', $contentType)); + } + + return $contentType; + } + + /** + * Returns the mime type definition for an alias + * + * e.g `getMimeType('pdf'); // returns 'application/pdf'` + * + * @param string $alias the content type alias to map + * @return mixed String mapped mime type or false if $alias is not mapped + */ + public function getMimeType($alias) + { + if (isset($this->_mimeTypes[$alias])) { + return $this->_mimeTypes[$alias]; + } + + return false; + } + + /** + * Maps a content-type back to an alias + * + * e.g `mapType('application/pdf'); // returns 'pdf'` + * + * @param string|array $ctype Either a string content type to map, or an array of types. + * @return string|array|null Aliases for the types provided. + */ + public function mapType($ctype) + { + if (is_array($ctype)) { + return array_map([$this, 'mapType'], $ctype); + } + + foreach ($this->_mimeTypes as $alias => $types) { + if (in_array($ctype, (array)$types)) { + return $alias; + } + } + + return null; + } + + /** + * Sets the response charset + * if $charset is null the current charset is returned + * + * @param string|null $charset Character set string. + * @return string Current charset + * @deprecated 3.5.0 Use getCharset()/withCharset() instead. + */ + public function charset($charset = null) + { + deprecationWarning( + 'Response::charset() is deprecated. ' . + 'Use getCharset()/withCharset() instead.' + ); + + if ($charset === null) { + return $this->_charset; + } + $this->_charset = $charset; + $this->_setContentType(); + + return $this->_charset; + } + + /** + * Returns the current charset. + * + * @return string + */ + public function getCharset() + { + return $this->_charset; + } + + /** + * Get a new instance with an updated charset. + * + * @param string $charset Character set string. + * @return static + */ + public function withCharset($charset) + { + $new = clone $this; + $new->_charset = $charset; + $new->_setContentType(); + + return $new; + } + + /** + * Sets the correct headers to instruct the client to not cache the response + * + * @return void + * @deprecated 3.4.0 Use withDisabledCache() instead. + */ + public function disableCache() + { + deprecationWarning( + 'Response::disableCache() is deprecated. ' . + 'Use withDisabledCache() instead.' + ); + + $this->_setHeader('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT'); + $this->_setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT'); + $this->_setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'); + } + + /** + * Create a new instance with headers to instruct the client to not cache the response + * + * @return static + */ + public function withDisabledCache() + { + return $this->withHeader('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT') + ->withHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT') + ->withHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'); + } + + /** + * Sets the correct headers to instruct the client to cache the response. + * + * @param string $since a valid time since the response text has not been modified + * @param string $time a valid time for cache expiry + * @return void + * @deprecated 3.4.0 Use withCache() instead. + */ + public function cache($since, $time = '+1 day') + { + deprecationWarning( + 'Response::cache() is deprecated. ' . + 'Use withCache() instead.' + ); + + if (!is_int($time)) { + $time = strtotime($time); + } + + $this->_setHeader('Date', gmdate('D, j M Y G:i:s ', time()) . 'GMT'); + + $this->modified($since); + $this->expires($time); + $this->sharable(true); + $this->maxAge($time - time()); + } + + /** + * Create a new instance with the headers to enable client caching. + * + * @param string $since a valid time since the response text has not been modified + * @param string $time a valid time for cache expiry + * @return static + */ + public function withCache($since, $time = '+1 day') + { + if (!is_int($time)) { + $time = strtotime($time); + } + + return $this->withHeader('Date', gmdate('D, j M Y G:i:s ', time()) . 'GMT') + ->withModified($since) + ->withExpires($time) + ->withSharable(true) + ->withMaxAge($time - time()); + } + + /** + * Sets whether a response is eligible to be cached by intermediate proxies + * This method controls the `public` or `private` directive in the Cache-Control + * header + * + * @param bool|null $public If set to true, the Cache-Control header will be set as public + * if set to false, the response will be set to private + * if no value is provided, it will return whether the response is sharable or not + * @param int|null $time time in seconds after which the response should no longer be considered fresh + * @return bool|null + */ + public function sharable($public = null, $time = null) + { + deprecationWarning( + 'Response::sharable() is deprecated. ' . + 'Use withSharable() instead.' + ); + if ($public === null) { + $public = array_key_exists('public', $this->_cacheDirectives); + $private = array_key_exists('private', $this->_cacheDirectives); + $noCache = array_key_exists('no-cache', $this->_cacheDirectives); + if (!$public && !$private && !$noCache) { + return null; + } + + return $public || !($private || $noCache); + } + if ($public) { + $this->_cacheDirectives['public'] = true; + unset($this->_cacheDirectives['private']); + } else { + $this->_cacheDirectives['private'] = true; + unset($this->_cacheDirectives['public']); + } + + $this->maxAge($time); + if (!$time) { + $this->_setCacheControl(); + } + + return (bool)$public; + } + + /** + * Create a new instace with the public/private Cache-Control directive set. + * + * @param bool $public If set to true, the Cache-Control header will be set as public + * if set to false, the response will be set to private. + * @param int|null $time time in seconds after which the response should no longer be considered fresh. + * @return static + */ + public function withSharable($public, $time = null) + { + $new = clone $this; + unset($new->_cacheDirectives['private'], $new->_cacheDirectives['public']); + + $key = $public ? 'public' : 'private'; + $new->_cacheDirectives[$key] = true; + + if ($time !== null) { + $new->_cacheDirectives['max-age'] = $time; + } + $new->_setCacheControl(); + + return $new; + } + + /** + * Sets the Cache-Control s-maxage directive. + * + * The max-age is the number of seconds after which the response should no longer be considered + * a good candidate to be fetched from a shared cache (like in a proxy server). + * If called with no parameters, this function will return the current max-age value if any + * + * @deprecated 3.6.5 Use withSharedMaxAge() instead. + * @param int|null $seconds if null, the method will return the current s-maxage value + * @return int|null + */ + public function sharedMaxAge($seconds = null) + { + deprecationWarning( + 'Response::sharedMaxAge() is deprecated. ' . + 'Use withSharedMaxAge() instead.' + ); + if ($seconds !== null) { + $this->_cacheDirectives['s-maxage'] = $seconds; + $this->_setCacheControl(); + } + if (isset($this->_cacheDirectives['s-maxage'])) { + return $this->_cacheDirectives['s-maxage']; + } + + return null; + } + + /** + * Create a new instance with the Cache-Control s-maxage directive. + * + * The max-age is the number of seconds after which the response should no longer be considered + * a good candidate to be fetched from a shared cache (like in a proxy server). + * + * @param int $seconds The number of seconds for shared max-age + * @return static + */ + public function withSharedMaxAge($seconds) + { + $new = clone $this; + $new->_cacheDirectives['s-maxage'] = $seconds; + $new->_setCacheControl(); + + return $new; + } + + /** + * Sets the Cache-Control max-age directive. + * The max-age is the number of seconds after which the response should no longer be considered + * a good candidate to be fetched from the local (client) cache. + * If called with no parameters, this function will return the current max-age value if any + * + * @deprecated 3.6.5 Use withMaxAge() instead. + * @param int|null $seconds if null, the method will return the current max-age value + * @return int|null + */ + public function maxAge($seconds = null) + { + deprecationWarning( + 'Response::maxAge() is deprecated. ' . + 'Use withMaxAge() instead.' + ); + if ($seconds !== null) { + $this->_cacheDirectives['max-age'] = $seconds; + $this->_setCacheControl(); + } + if (isset($this->_cacheDirectives['max-age'])) { + return $this->_cacheDirectives['max-age']; + } + + return null; + } + + /** + * Create an instance with Cache-Control max-age directive set. + * + * The max-age is the number of seconds after which the response should no longer be considered + * a good candidate to be fetched from the local (client) cache. + * + * @param int $seconds The seconds a cached response can be considered valid + * @return static + */ + public function withMaxAge($seconds) + { + $new = clone $this; + $new->_cacheDirectives['max-age'] = $seconds; + $new->_setCacheControl(); + + return $new; + } + + /** + * Sets the Cache-Control must-revalidate directive. + * must-revalidate indicates that the response should not be served + * stale by a cache under any circumstance without first revalidating + * with the origin. + * If called with no parameters, this function will return whether must-revalidate is present. + * + * @param bool|null $enable if null, the method will return the current + * must-revalidate value. If boolean sets or unsets the directive. + * @return bool + * @deprecated 3.4.0 Use withMustRevalidate() instead. + */ + public function mustRevalidate($enable = null) + { + deprecationWarning( + 'Response::mustRevalidate() is deprecated. ' . + 'Use withMustRevalidate() instead.' + ); + + if ($enable !== null) { + if ($enable) { + $this->_cacheDirectives['must-revalidate'] = true; + } else { + unset($this->_cacheDirectives['must-revalidate']); + } + $this->_setCacheControl(); + } + + return array_key_exists('must-revalidate', $this->_cacheDirectives); + } + + /** + * Create an instance with Cache-Control must-revalidate directive set. + * + * Sets the Cache-Control must-revalidate directive. + * must-revalidate indicates that the response should not be served + * stale by a cache under any circumstance without first revalidating + * with the origin. + * + * @param bool $enable If boolean sets or unsets the directive. + * @return static + */ + public function withMustRevalidate($enable) + { + $new = clone $this; + if ($enable) { + $new->_cacheDirectives['must-revalidate'] = true; + } else { + unset($new->_cacheDirectives['must-revalidate']); + } + $new->_setCacheControl(); + + return $new; + } + + /** + * Helper method to generate a valid Cache-Control header from the options set + * in other methods + * + * @return void + */ + protected function _setCacheControl() + { + $control = ''; + foreach ($this->_cacheDirectives as $key => $val) { + $control .= $val === true ? $key : sprintf('%s=%s', $key, $val); + $control .= ', '; + } + $control = rtrim($control, ', '); + $this->_setHeader('Cache-Control', $control); + } + + /** + * Sets the Expires header for the response by taking an expiration time + * If called with no parameters it will return the current Expires value + * + * ### Examples: + * + * `$response->expires('now')` Will Expire the response cache now + * `$response->expires(new DateTime('+1 day'))` Will set the expiration in next 24 hours + * `$response->expires()` Will return the current expiration header value + * + * @param string|\DateTime|null $time Valid time string or \DateTime instance. + * @return string|null + * @deprecated 3.4.0 Use withExpires() instead. + */ + public function expires($time = null) + { + deprecationWarning( + 'Response::expires() is deprecated. ' . + 'Use withExpires() instead.' + ); + + if ($time !== null) { + $date = $this->_getUTCDate($time); + $this->_setHeader('Expires', $date->format('D, j M Y H:i:s') . ' GMT'); + } + + if ($this->hasHeader('Expires')) { + return $this->getHeaderLine('Expires'); + } + + return null; + } + + /** + * Create a new instance with the Expires header set. + * + * ### Examples: + * + * ``` + * // Will Expire the response cache now + * $response->withExpires('now') + * + * // Will set the expiration in next 24 hours + * $response->withExpires(new DateTime('+1 day')) + * ``` + * + * @param string|\DateTime $time Valid time string or \DateTime instance. + * @return static + */ + public function withExpires($time) + { + $date = $this->_getUTCDate($time); + + return $this->withHeader('Expires', $date->format('D, j M Y H:i:s') . ' GMT'); + } + + /** + * Sets the Last-Modified header for the response by taking a modification time + * If called with no parameters it will return the current Last-Modified value + * + * ### Examples: + * + * `$response->modified('now')` Will set the Last-Modified to the current time + * `$response->modified(new DateTime('+1 day'))` Will set the modification date in the past 24 hours + * `$response->modified()` Will return the current Last-Modified header value + * + * @param string|\DateTime|null $time Valid time string or \DateTime instance. + * @return string|null + * @deprecated 3.4.0 Use withModified() instead. + */ + public function modified($time = null) + { + deprecationWarning( + 'Response::modified() is deprecated. ' . + 'Use withModified() or getHeaderLine("Last-Modified") instead.' + ); + + if ($time !== null) { + $date = $this->_getUTCDate($time); + $this->_setHeader('Last-Modified', $date->format('D, j M Y H:i:s') . ' GMT'); + } + + if ($this->hasHeader('Last-Modified')) { + return $this->getHeaderLine('Last-Modified'); + } + + return null; + } + + /** + * Create a new instance with the Last-Modified header set. + * + * ### Examples: + * + * ``` + * // Will Expire the response cache now + * $response->withModified('now') + * + * // Will set the expiration in next 24 hours + * $response->withModified(new DateTime('+1 day')) + * ``` + * + * @param string|\DateTime $time Valid time string or \DateTime instance. + * @return static + */ + public function withModified($time) + { + $date = $this->_getUTCDate($time); + + return $this->withHeader('Last-Modified', $date->format('D, j M Y H:i:s') . ' GMT'); + } + + /** + * Sets the response as Not Modified by removing any body contents + * setting the status code to "304 Not Modified" and removing all + * conflicting headers + * + * *Warning* This method mutates the response in-place and should be avoided. + * + * @return void + */ + public function notModified() + { + $this->_createStream(); + $this->_setStatus(304); + + $remove = [ + 'Allow', + 'Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-MD5', + 'Content-Type', + 'Last-Modified' + ]; + foreach ($remove as $header) { + $this->_clearHeader($header); + } + } + + /** + * Create a new instance as 'not modified' + * + * This will remove any body contents set the status code + * to "304" and removing headers that describe + * a response body. + * + * @return static + */ + public function withNotModified() + { + $new = $this->withStatus(304); + $new->_createStream(); + $remove = [ + 'Allow', + 'Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-MD5', + 'Content-Type', + 'Last-Modified' + ]; + foreach ($remove as $header) { + $new = $new->withoutHeader($header); + } + + return $new; + } + + /** + * Sets the Vary header for the response, if an array is passed, + * values will be imploded into a comma separated string. If no + * parameters are passed, then an array with the current Vary header + * value is returned + * + * @param string|array|null $cacheVariances A single Vary string or an array + * containing the list for variances. + * @return array|null + * @deprecated 3.4.0 Use withVary() instead. + */ + public function vary($cacheVariances = null) + { + deprecationWarning( + 'Response::vary() is deprecated. ' . + 'Use withVary() instead.' + ); + + if ($cacheVariances !== null) { + $cacheVariances = (array)$cacheVariances; + $this->_setHeader('Vary', implode(', ', $cacheVariances)); + } + + if ($this->hasHeader('Vary')) { + return explode(', ', $this->getHeaderLine('Vary')); + } + + return null; + } + + /** + * Create a new instance with the Vary header set. + * + * If an array is passed values will be imploded into a comma + * separated string. If no parameters are passed, then an + * array with the current Vary header value is returned + * + * @param string|array $cacheVariances A single Vary string or an array + * containing the list for variances. + * @return static + */ + public function withVary($cacheVariances) + { + return $this->withHeader('Vary', (array)$cacheVariances); + } + + /** + * Sets the response Etag, Etags are a strong indicative that a response + * can be cached by a HTTP client. A bad way of generating Etags is + * creating a hash of the response output, instead generate a unique + * hash of the unique components that identifies a request, such as a + * modification time, a resource Id, and anything else you consider it + * makes it unique. + * + * Second parameter is used to instruct clients that the content has + * changed, but semantically, it can be used as the same thing. Think + * for instance of a page with a hit counter, two different page views + * are equivalent, but they differ by a few bytes. This leaves off to + * the Client the decision of using or not the cached page. + * + * If no parameters are passed, current Etag header is returned. + * + * @param string|null $hash The unique hash that identifies this response + * @param bool $weak Whether the response is semantically the same as + * other with the same hash or not + * @return string|null + * @deprecated 3.4.0 Use withEtag() instead. + */ + public function etag($hash = null, $weak = false) + { + deprecationWarning( + 'Response::etag() is deprecated. ' . + 'Use withEtag() or getHeaderLine("Etag") instead.' + ); + + if ($hash !== null) { + $this->_setHeader('Etag', sprintf('%s"%s"', $weak ? 'W/' : null, $hash)); + } + + if ($this->hasHeader('Etag')) { + return $this->getHeaderLine('Etag'); + } + + return null; + } + + /** + * Create a new instance with the Etag header set. + * + * Etags are a strong indicative that a response can be cached by a + * HTTP client. A bad way of generating Etags is creating a hash of + * the response output, instead generate a unique hash of the + * unique components that identifies a request, such as a + * modification time, a resource Id, and anything else you consider it + * that makes the response unique. + * + * The second parameter is used to inform clients that the content has + * changed, but semantically it is equivalent to existing cached values. Consider + * a page with a hit counter, two different page views are equivalent, but + * they differ by a few bytes. This permits the Client to decide whether they should + * use the cached data. + * + * @param string $hash The unique hash that identifies this response + * @param bool $weak Whether the response is semantically the same as + * other with the same hash or not. Defaults to false + * @return static + */ + public function withEtag($hash, $weak = false) + { + $hash = sprintf('%s"%s"', $weak ? 'W/' : null, $hash); + + return $this->withHeader('Etag', $hash); + } + + /** + * Returns a DateTime object initialized at the $time param and using UTC + * as timezone + * + * @param string|int|\DateTime|null $time Valid time string or \DateTime instance. + * @return \DateTime + */ + protected function _getUTCDate($time = null) + { + if ($time instanceof DateTime) { + $result = clone $time; + } elseif (is_int($time)) { + $result = new DateTime(date('Y-m-d H:i:s', $time)); + } else { + $result = new DateTime($time); + } + $result->setTimezone(new DateTimeZone('UTC')); + + return $result; + } + + /** + * Sets the correct output buffering handler to send a compressed response. Responses will + * be compressed with zlib, if the extension is available. + * + * @return bool false if client does not accept compressed responses or no handler is available, true otherwise + */ + public function compress() + { + $compressionEnabled = ini_get('zlib.output_compression') !== '1' && + extension_loaded('zlib') && + (strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false); + + return $compressionEnabled && ob_start('ob_gzhandler'); + } + + /** + * Returns whether the resulting output will be compressed by PHP + * + * @return bool + */ + public function outputCompressed() + { + return strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false + && (ini_get('zlib.output_compression') === '1' || in_array('ob_gzhandler', ob_list_handlers())); + } + + /** + * Sets the correct headers to instruct the browser to download the response as a file. + * + * @param string $filename The name of the file as the browser will download the response + * @return void + * @deprecated 3.4.0 Use withDownload() instead. + */ + public function download($filename) + { + deprecationWarning( + 'Response::download() is deprecated. ' . + 'Use withDownload() instead.' + ); + + $this->header('Content-Disposition', 'attachment; filename="' . $filename . '"'); + } + + /** + * Create a new instance with the Content-Disposition header set. + * + * @param string $filename The name of the file as the browser will download the response + * @return static + */ + public function withDownload($filename) + { + return $this->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); + } + + /** + * Sets the protocol to be used when sending the response. Defaults to HTTP/1.1 + * If called with no arguments, it will return the current configured protocol + * + * @param string|null $protocol Protocol to be used for sending response. + * @return string Protocol currently set + * @deprecated 3.4.0 Use getProtocolVersion() instead. + */ + public function protocol($protocol = null) + { + deprecationWarning( + 'Response::protocol() is deprecated. ' . + 'Use getProtocolVersion() instead.' + ); + + if ($protocol !== null) { + $this->_protocol = $protocol; + } + + return $this->_protocol; + } + + /** + * Sets the Content-Length header for the response + * If called with no arguments returns the last Content-Length set + * + * @param int|null $bytes Number of bytes + * @return string|null + * @deprecated 3.4.0 Use withLength() to set length instead. + */ + public function length($bytes = null) + { + deprecationWarning( + 'Response::length() is deprecated. ' . + 'Use withLength() instead.' + ); + + if ($bytes !== null) { + $this->_setHeader('Content-Length', $bytes); + } + + if ($this->hasHeader('Content-Length')) { + return $this->getHeaderLine('Content-Length'); + } + + return null; + } + + /** + * Create a new response with the Content-Length header set. + * + * @param int|string $bytes Number of bytes + * @return static + */ + public function withLength($bytes) + { + return $this->withHeader('Content-Length', (string)$bytes); + } + + /** + * Create a new response with the Link header set. + * + * ### Examples + * + * ``` + * $response = $response->withAddedLink('http://example.com?page=1', ['rel' => 'prev']) + * ->withAddedLink('http://example.com?page=3', ['rel' => 'next']); + * ``` + * + * Will generate: + * + * ``` + * Link: ; rel="prev" + * Link: ; rel="next" + * ``` + * + * @param string $url The LinkHeader url. + * @param array $options The LinkHeader params. + * @return static + * @since 3.6.0 + */ + public function withAddedLink($url, $options = []) + { + $params = []; + foreach ($options as $key => $option) { + $params[] = $key . '="' . $option . '"'; + } + + $param = ''; + if ($params) { + $param = '; ' . implode('; ', $params); + } + + return $this->withAddedHeader('Link', '<' . $url . '>' . $param); + } + + /** + * Checks whether a response has not been modified according to the 'If-None-Match' + * (Etags) and 'If-Modified-Since' (last modification date) request + * headers. If the response is detected to be not modified, it + * is marked as so accordingly so the client can be informed of that. + * + * In order to mark a response as not modified, you need to set at least + * the Last-Modified etag response header before calling this method. Otherwise + * a comparison will not be possible. + * + * *Warning* This method mutates the response in-place and should be avoided. + * + * @param \Cake\Http\ServerRequest $request Request object + * @return bool Whether the response was marked as not modified or not. + */ + public function checkNotModified(ServerRequest $request) + { + $etags = preg_split('/\s*,\s*/', (string)$request->getHeaderLine('If-None-Match'), 0, PREG_SPLIT_NO_EMPTY); + $responseTag = $this->getHeaderLine('Etag'); + if ($responseTag) { + $etagMatches = in_array('*', $etags) || in_array($responseTag, $etags); + } + + $modifiedSince = $request->getHeaderLine('If-Modified-Since'); + if ($modifiedSince && $this->hasHeader('Last-Modified')) { + $timeMatches = strtotime($this->getHeaderLine('Last-Modified')) === strtotime($modifiedSince); + } + $checks = compact('etagMatches', 'timeMatches'); + if (empty($checks)) { + return false; + } + $notModified = !in_array(false, $checks, true); + if ($notModified) { + $this->notModified(); + } + + return $notModified; + } + + /** + * String conversion. Fetches the response body as a string. + * Does *not* send headers. + * If body is a callable, a blank string is returned. + * + * @return string + */ + public function __toString() + { + $this->stream->rewind(); + + return (string)$this->stream->getContents(); + } + + /** + * Getter/Setter for cookie configs + * + * This method acts as a setter/getter depending on the type of the argument. + * If the method is called with no arguments, it returns all configurations. + * + * If the method is called with a string as argument, it returns either the + * given configuration if it is set, or null, if it's not set. + * + * If the method is called with an array as argument, it will set the cookie + * configuration to the cookie container. + * + * ### Options (when setting a configuration) + * - name: The Cookie name + * - value: Value of the cookie + * - expire: Time the cookie expires in + * - path: Path the cookie applies to + * - domain: Domain the cookie is for. + * - secure: Is the cookie https? + * - httpOnly: Is the cookie available in the client? + * + * ### Examples + * + * ### Getting all cookies + * + * `$this->cookie()` + * + * ### Getting a certain cookie configuration + * + * `$this->cookie('MyCookie')` + * + * ### Setting a cookie configuration + * + * `$this->cookie((array) $options)` + * + * @param array|null $options Either null to get all cookies, string for a specific cookie + * or array to set cookie. + * @return mixed + * @deprecated 3.4.0 Use getCookie(), getCookies() and withCookie() instead. + */ + public function cookie($options = null) + { + deprecationWarning( + 'Response::cookie() is deprecated. ' . + 'Use getCookie(), getCookies() and withCookie() instead.' + ); + + if ($options === null) { + return $this->getCookies(); + } + + if (is_string($options)) { + if (!$this->_cookies->has($options)) { + return null; + } + + $cookie = $this->_cookies->get($options); + + return $this->convertCookieToArray($cookie); + } + + $options += [ + 'name' => 'CakeCookie[default]', + 'value' => '', + 'expire' => 0, + 'path' => '/', + 'domain' => '', + 'secure' => false, + 'httpOnly' => false + ]; + $expires = $options['expire'] ? new DateTime('@' . $options['expire']) : null; + $cookie = new Cookie( + $options['name'], + $options['value'], + $expires, + $options['path'], + $options['domain'], + $options['secure'], + $options['httpOnly'] + ); + $this->_cookies = $this->_cookies->add($cookie); + } + + /** + * Create a new response with a cookie set. + * + * ### Options + * + * - `value`: Value of the cookie + * - `expire`: Time the cookie expires in + * - `path`: Path the cookie applies to + * - `domain`: Domain the cookie is for. + * - `secure`: Is the cookie https? + * - `httpOnly`: Is the cookie available in the client? + * + * ### Examples + * + * ``` + * // set scalar value with defaults + * $response = $response->withCookie('remember_me', 1); + * + * // customize cookie attributes + * $response = $response->withCookie('remember_me', ['path' => '/login']); + * + * // add a cookie object + * $response = $response->withCookie(new Cookie('remember_me', 1)); + * ``` + * + * @param string|\Cake\Http\Cookie\Cookie $name The name of the cookie to set, or a cookie object + * @param array|string $data Either a string value, or an array of cookie options. + * @return static + */ + public function withCookie($name, $data = '') + { + if ($name instanceof Cookie) { + $cookie = $name; + } else { + if (!is_array($data)) { + $data = ['value' => $data]; + } + $data += [ + 'value' => '', + 'expire' => 0, + 'path' => '/', + 'domain' => '', + 'secure' => false, + 'httpOnly' => false + ]; + $expires = $data['expire'] ? new DateTime('@' . $data['expire']) : null; + $cookie = new Cookie( + $name, + $data['value'], + $expires, + $data['path'], + $data['domain'], + $data['secure'], + $data['httpOnly'] + ); + } + + $new = clone $this; + $new->_cookies = $new->_cookies->add($cookie); + + return $new; + } + + /** + * Create a new response with an expired cookie set. + * + * ### Options + * + * - `path`: Path the cookie applies to + * - `domain`: Domain the cookie is for. + * - `secure`: Is the cookie https? + * - `httpOnly`: Is the cookie available in the client? + * + * ### Examples + * + * ``` + * // set scalar value with defaults + * $response = $response->withExpiredCookie('remember_me'); + * + * // customize cookie attributes + * $response = $response->withExpiredCookie('remember_me', ['path' => '/login']); + * + * // add a cookie object + * $response = $response->withExpiredCookie(new Cookie('remember_me')); + * ``` + * + * @param string|\Cake\Http\Cookie\CookieInterface $name The name of the cookie to expire, or a cookie object + * @param array $options An array of cookie options. + * @return static + */ + public function withExpiredCookie($name, $options = []) + { + if ($name instanceof CookieInterface) { + $cookie = $name->withExpired(); + } else { + $options += [ + 'path' => '/', + 'domain' => '', + 'secure' => false, + 'httpOnly' => false + ]; + + $cookie = new Cookie( + $name, + '', + DateTime::createFromFormat('U', 1), + $options['path'], + $options['domain'], + $options['secure'], + $options['httpOnly'] + ); + } + + $new = clone $this; + $new->_cookies = $new->_cookies->add($cookie); + + return $new; + } + + /** + * Read a single cookie from the response. + * + * This method provides read access to pending cookies. It will + * not read the `Set-Cookie` header if set. + * + * @param string $name The cookie name you want to read. + * @return array|null Either the cookie data or null + */ + public function getCookie($name) + { + if (!$this->_cookies->has($name)) { + return null; + } + + $cookie = $this->_cookies->get($name); + + return $this->convertCookieToArray($cookie); + } + + /** + * Get all cookies in the response. + * + * Returns an associative array of cookie name => cookie data. + * + * @return array + */ + public function getCookies() + { + $out = []; + foreach ($this->_cookies as $cookie) { + $out[$cookie->getName()] = $this->convertCookieToArray($cookie); + } + + return $out; + } + + /** + * Convert the cookie into an array of its properties. + * + * This method is compatible with the historical behavior of Cake\Http\Response, + * where `httponly` is `httpOnly` and `expires` is `expire` + * + * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie object. + * @return array + */ + protected function convertCookieToArray(CookieInterface $cookie) + { + return [ + 'name' => $cookie->getName(), + 'value' => $cookie->getStringValue(), + 'path' => $cookie->getPath(), + 'domain' => $cookie->getDomain(), + 'secure' => $cookie->isSecure(), + 'httpOnly' => $cookie->isHttpOnly(), + 'expire' => $cookie->getExpiresTimestamp() + ]; + } + + /** + * Get the CookieCollection from the response + * + * @return \Cake\Http\Cookie\CookieCollection + */ + public function getCookieCollection() + { + return $this->_cookies; + } + + /** + * Setup access for origin and methods on cross origin requests + * + * This method allow multiple ways to setup the domains, see the examples + * + * ### Full URI + * ``` + * cors($request, 'https://www.cakephp.org'); + * ``` + * + * ### URI with wildcard + * ``` + * cors($request, 'https://*.cakephp.org'); + * ``` + * + * ### Ignoring the requested protocol + * ``` + * cors($request, 'www.cakephp.org'); + * ``` + * + * ### Any URI + * ``` + * cors($request, '*'); + * ``` + * + * ### Whitelist of URIs + * ``` + * cors($request, ['http://www.cakephp.org', '*.google.com', 'https://myproject.github.io']); + * ``` + * + * *Note* The `$allowedDomains`, `$allowedMethods`, `$allowedHeaders` parameters are deprecated. + * Instead the builder object should be used. + * + * @param \Cake\Http\ServerRequest $request Request object + * @param string|array $allowedDomains List of allowed domains, see method description for more details + * @param string|array $allowedMethods List of HTTP verbs allowed + * @param string|array $allowedHeaders List of HTTP headers allowed + * @return \Cake\Http\CorsBuilder A builder object the provides a fluent interface for defining + * additional CORS headers. + */ + public function cors(ServerRequest $request, $allowedDomains = [], $allowedMethods = [], $allowedHeaders = []) + { + $origin = $request->getHeaderLine('Origin'); + $ssl = $request->is('ssl'); + $builder = new CorsBuilder($this, $origin, $ssl); + if (!$origin) { + return $builder; + } + if (empty($allowedDomains) && empty($allowedMethods) && empty($allowedHeaders)) { + return $builder; + } + deprecationWarning( + 'The $allowedDomains, $allowedMethods, and $allowedHeaders parameters of Response::cors() ' . + 'are deprecated. Instead you should use the builder methods on the return of cors().' + ); + + $updated = $builder->allowOrigin($allowedDomains) + ->allowMethods((array)$allowedMethods) + ->allowHeaders((array)$allowedHeaders) + ->build(); + + // If $updated is a new instance, mutate this object in-place + // to retain existing behavior. + if ($updated !== $this) { + foreach ($updated->getHeaders() as $name => $values) { + if (!$this->hasHeader($name)) { + $this->_setHeader($name, $values[0]); + } + } + } + + return $builder; + } + + /** + * Setup for display or download the given file. + * + * If $_SERVER['HTTP_RANGE'] is set a slice of the file will be + * returned instead of the entire file. + * + * ### Options keys + * + * - name: Alternate download name + * - download: If `true` sets download header and forces file to be downloaded rather than displayed in browser + * + * @param string $path Path to file. If the path is not an absolute path that resolves + * to a file, `APP` will be prepended to the path (this behavior is deprecated). + * @param array $options Options See above. + * @return void + * @throws \Cake\Http\Exception\NotFoundException + * @deprecated 3.4.0 Use withFile() instead. + */ + public function file($path, array $options = []) + { + deprecationWarning( + 'Response::file() is deprecated. ' . + 'Use withFile() instead.' + ); + + $file = $this->validateFile($path); + $options += [ + 'name' => null, + 'download' => null + ]; + + $extension = strtolower($file->ext()); + $download = $options['download']; + if ((!$extension || $this->type($extension) === false) && $download === null) { + $download = true; + } + + $fileSize = $file->size(); + if ($download) { + $agent = env('HTTP_USER_AGENT'); + + if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent)) { + $contentType = 'application/octet-stream'; + } elseif (preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) { + $contentType = 'application/force-download'; + } + + if (!empty($contentType)) { + $this->type($contentType); + } + if ($options['name'] === null) { + $name = $file->name; + } else { + $name = $options['name']; + } + $this->download($name); + $this->header('Content-Transfer-Encoding', 'binary'); + } + + $this->header('Accept-Ranges', 'bytes'); + $httpRange = env('HTTP_RANGE'); + if (isset($httpRange)) { + $this->_fileRange($file, $httpRange); + } else { + $this->header('Content-Length', $fileSize); + } + + $this->_file = $file; + $this->stream = new Stream($file->path, 'rb'); + } + + /** + * Create a new instance that is based on a file. + * + * This method will augment both the body and a number of related headers. + * + * If `$_SERVER['HTTP_RANGE']` is set, a slice of the file will be + * returned instead of the entire file. + * + * ### Options keys + * + * - name: Alternate download name + * - download: If `true` sets download header and forces file to + * be downloaded rather than displayed inline. + * + * @param string $path Path to file. If the path is not an absolute path that resolves + * to a file, `APP` will be prepended to the path (this behavior is deprecated). + * @param array $options Options See above. + * @return static + * @throws \Cake\Http\Exception\NotFoundException + */ + public function withFile($path, array $options = []) + { + $file = $this->validateFile($path); + $options += [ + 'name' => null, + 'download' => null + ]; + + $extension = strtolower($file->ext()); + $mapped = $this->getMimeType($extension); + if ((!$extension || !$mapped) && $options['download'] === null) { + $options['download'] = true; + } + + $new = clone $this; + if ($mapped) { + $new = $new->withType($extension); + } + + $fileSize = $file->size(); + if ($options['download']) { + $agent = env('HTTP_USER_AGENT'); + + if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent)) { + $contentType = 'application/octet-stream'; + } elseif (preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) { + $contentType = 'application/force-download'; + } + + if (isset($contentType)) { + $new = $new->withType($contentType); + } + $name = $options['name'] ?: $file->name; + $new = $new->withDownload($name) + ->withHeader('Content-Transfer-Encoding', 'binary'); + } + + $new = $new->withHeader('Accept-Ranges', 'bytes'); + $httpRange = env('HTTP_RANGE'); + if (isset($httpRange)) { + $new->_fileRange($file, $httpRange); + } else { + $new = $new->withHeader('Content-Length', (string)$fileSize); + } + $new->_file = $file; + $new->stream = new Stream($file->path, 'rb'); + + return $new; + } + + /** + * Convenience method to set a string into the response body + * + * @param string $string The string to be sent + * @return static + */ + public function withStringBody($string) + { + $new = clone $this; + $new->_createStream(); + $new->stream->write((string)$string); + + return $new; + } + + /** + * Validate a file path is a valid response body. + * + * @param string $path The path to the file. + * @throws \Cake\Http\Exception\NotFoundException + * @return \Cake\Filesystem\File + */ + protected function validateFile($path) + { + if (strpos($path, '../') !== false || strpos($path, '..\\') !== false) { + throw new NotFoundException(__d('cake', 'The requested file contains `..` and will not be read.')); + } + if (!is_file($path)) { + deprecationWarning( + 'Automatic prefixing of paths with `APP` by `Response::file()` and `withFile()` is deprecated. ' . + 'Use absolute paths instead.' + ); + $path = APP . $path; + } + if (!Folder::isAbsolute($path)) { + deprecationWarning( + 'Serving files via `file()` or `withFile()` using relative paths is deprecated.' . + 'Use an absolute path instead.' + ); + } + + $file = new File($path); + if (!$file->exists() || !$file->readable()) { + if (Configure::read('debug')) { + throw new NotFoundException(sprintf('The requested file %s was not found or not readable', $path)); + } + throw new NotFoundException(__d('cake', 'The requested file was not found')); + } + + return $file; + } + + /** + * Get the current file if one exists. + * + * @return \Cake\Filesystem\File|null The file to use in the response or null + */ + public function getFile() + { + return $this->_file; + } + + /** + * Apply a file range to a file and set the end offset. + * + * If an invalid range is requested a 416 Status code will be used + * in the response. + * + * @param \Cake\Filesystem\File $file The file to set a range on. + * @param string $httpRange The range to use. + * @return void + * @deprecated 3.4.0 Long term this needs to be refactored to follow immutable paradigms. + * However for now, it is simpler to leave this alone. + */ + protected function _fileRange($file, $httpRange) + { + $fileSize = $file->size(); + $lastByte = $fileSize - 1; + $start = 0; + $end = $lastByte; + + preg_match('/^bytes\s*=\s*(\d+)?\s*-\s*(\d+)?$/', $httpRange, $matches); + if ($matches) { + $start = $matches[1]; + $end = isset($matches[2]) ? $matches[2] : ''; + } + + if ($start === '') { + $start = $fileSize - $end; + $end = $lastByte; + } + if ($end === '') { + $end = $lastByte; + } + + if ($start > $end || $end > $lastByte || $start > $lastByte) { + $this->_setStatus(416); + $this->_setHeader('Content-Range', 'bytes 0-' . $lastByte . '/' . $fileSize); + + return; + } + + $this->_setHeader('Content-Length', $end - $start + 1); + $this->_setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $fileSize); + $this->_setStatus(206); + $this->_fileRange = [$start, $end]; + } + + /** + * Reads out a file, and echos the content to the client. + * + * @param \Cake\Filesystem\File $file File object + * @param array $range The range to read out of the file. + * @return bool True is whole file is echoed successfully or false if client connection is lost in between + * @deprecated 3.4.0 Will be removed in 4.0.0 + */ + protected function _sendFile($file, $range) + { + deprecationWarning('Will be removed in 4.0.0'); + + ob_implicit_flush(true); + + $file->open('rb'); + + $end = $start = false; + if ($range) { + list($start, $end) = $range; + } + if ($start !== false) { + $file->offset($start); + } + + $bufferSize = 8192; + set_time_limit(0); + session_write_close(); + while (!feof($file->handle)) { + if (!$this->_isActive()) { + $file->close(); + + return false; + } + $offset = $file->offset(); + if ($end && $offset >= $end) { + break; + } + if ($end && $offset + $bufferSize >= $end) { + $bufferSize = $end - $offset + 1; + } + echo fread($file->handle, $bufferSize); + } + $file->close(); + + return true; + } + + /** + * Returns true if connection is still active + * + * @return bool + * @deprecated 3.4.0 Will be removed in 4.0.0 + */ + protected function _isActive() + { + deprecationWarning('Will be removed in 4.0.0'); + + return connection_status() === CONNECTION_NORMAL && !connection_aborted(); + } + + /** + * Clears the contents of the topmost output buffer and discards them + * + * @return bool + * @deprecated 3.2.4 This function is not needed anymore + */ + protected function _clearBuffer() + { + deprecationWarning( + 'This function is not needed anymore and will be removed.' + ); + + //@codingStandardsIgnoreStart + return @ob_end_clean(); + //@codingStandardsIgnoreEnd + } + + /** + * Flushes the contents of the output buffer + * + * @return void + * @deprecated 3.2.4 This function is not needed anymore + */ + protected function _flushBuffer() + { + deprecationWarning( + 'This function is not needed anymore and will be removed.' + ); + + //@codingStandardsIgnoreStart + @flush(); + if (ob_get_level()) { + @ob_flush(); + } + //@codingStandardsIgnoreEnd + } + + /** + * Stop execution of the current script. Wraps exit() making + * testing easier. + * + * @param int|string $status See https://secure.php.net/exit for values + * @return void + * @deprecated 3.4.0 Will be removed in 4.0.0 + */ + public function stop($status = 0) + { + deprecationWarning('Will be removed in 4.0.0'); + + exit($status); + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + return [ + 'status' => $this->_status, + 'contentType' => $this->_contentType, + 'headers' => $this->headers, + 'file' => $this->_file, + 'fileRange' => $this->_fileRange, + 'cookies' => $this->_cookies, + 'cacheDirectives' => $this->_cacheDirectives, + 'body' => (string)$this->getBody(), + ]; + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Http\Response', 'Cake\Network\Response'); diff --git a/app/vendor/cakephp/cakephp/src/Http/ResponseEmitter.php b/app/vendor/cakephp/cakephp/src/Http/ResponseEmitter.php new file mode 100644 index 000000000..9e320bc54 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/ResponseEmitter.php @@ -0,0 +1,294 @@ +emitStatusLine($response); + $this->emitHeaders($response); + $this->flush(); + + $range = $this->parseContentRange($response->getHeaderLine('Content-Range')); + if (is_array($range)) { + $this->emitBodyRange($range, $response, $maxBufferLength); + } else { + $this->emitBody($response, $maxBufferLength); + } + + if (function_exists('fastcgi_finish_request')) { + fastcgi_finish_request(); + } + } + + /** + * Emit the message body. + * + * @param \Psr\Http\Message\ResponseInterface $response The response to emit + * @param int $maxBufferLength The chunk size to emit + * @return void + */ + protected function emitBody(ResponseInterface $response, $maxBufferLength) + { + if (in_array($response->getStatusCode(), [204, 304])) { + return; + } + $body = $response->getBody(); + + if (!$body->isSeekable()) { + echo $body; + + return; + } + + $body->rewind(); + while (!$body->eof()) { + echo $body->read($maxBufferLength); + } + } + + /** + * Emit a range of the message body. + * + * @param array $range The range data to emit + * @param \Psr\Http\Message\ResponseInterface $response The response to emit + * @param int $maxBufferLength The chunk size to emit + * @return void + */ + protected function emitBodyRange(array $range, ResponseInterface $response, $maxBufferLength) + { + list($unit, $first, $last, $length) = $range; + + $body = $response->getBody(); + + if (!$body->isSeekable()) { + $contents = $body->getContents(); + echo substr($contents, $first, $last - $first + 1); + + return; + } + + $body = new RelativeStream($body, $first); + $body->rewind(); + $pos = 0; + $length = $last - $first + 1; + while (!$body->eof() && $pos < $length) { + if (($pos + $maxBufferLength) > $length) { + echo $body->read($length - $pos); + break; + } + + echo $body->read($maxBufferLength); + $pos = $body->tell(); + } + } + + /** + * Emit the status line. + * + * Emits the status line using the protocol version and status code from + * the response; if a reason phrase is available, it, too, is emitted. + * + * @param \Psr\Http\Message\ResponseInterface $response The response to emit + * @return void + */ + protected function emitStatusLine(ResponseInterface $response) + { + $reasonPhrase = $response->getReasonPhrase(); + header(sprintf( + 'HTTP/%s %d%s', + $response->getProtocolVersion(), + $response->getStatusCode(), + ($reasonPhrase ? ' ' . $reasonPhrase : '') + )); + } + + /** + * Emit response headers. + * + * Loops through each header, emitting each; if the header value + * is an array with multiple values, ensures that each is sent + * in such a way as to create aggregate headers (instead of replace + * the previous). + * + * @param \Psr\Http\Message\ResponseInterface $response The response to emit + * @return void + */ + protected function emitHeaders(ResponseInterface $response) + { + $cookies = []; + if (method_exists($response, 'getCookies')) { + $cookies = $response->getCookies(); + } + + foreach ($response->getHeaders() as $name => $values) { + if (strtolower($name) === 'set-cookie') { + $cookies = array_merge($cookies, $values); + continue; + } + $first = true; + foreach ($values as $value) { + header(sprintf( + '%s: %s', + $name, + $value + ), $first); + $first = false; + } + } + + $this->emitCookies($cookies); + } + + /** + * Emit cookies using setcookie() + * + * @param array $cookies An array of Set-Cookie headers. + * @return void + */ + protected function emitCookies(array $cookies) + { + foreach ($cookies as $cookie) { + if (is_array($cookie)) { + setcookie( + $cookie['name'], + $cookie['value'], + $cookie['expire'], + $cookie['path'], + $cookie['domain'], + $cookie['secure'], + $cookie['httpOnly'] + ); + continue; + } + + if (strpos($cookie, '";"') !== false) { + $cookie = str_replace('";"', '{__cookie_replace__}', $cookie); + $parts = str_replace('{__cookie_replace__}', '";"', explode(';', $cookie)); + } else { + $parts = preg_split('/\;[ \t]*/', $cookie); + } + + list($name, $value) = explode('=', array_shift($parts), 2); + $data = [ + 'name' => urldecode($name), + 'value' => urldecode($value), + 'expires' => 0, + 'path' => '', + 'domain' => '', + 'secure' => false, + 'httponly' => false + ]; + + foreach ($parts as $part) { + if (strpos($part, '=') !== false) { + list($key, $value) = explode('=', $part); + } else { + $key = $part; + $value = true; + } + + $key = strtolower($key); + $data[$key] = $value; + } + if (!empty($data['expires'])) { + $data['expires'] = strtotime($data['expires']); + } + setcookie( + $data['name'], + $data['value'], + $data['expires'], + $data['path'], + $data['domain'], + $data['secure'], + $data['httponly'] + ); + } + } + + /** + * Loops through the output buffer, flushing each, before emitting + * the response. + * + * @param int|null $maxBufferLevel Flush up to this buffer level. + * @return void + */ + protected function flush($maxBufferLevel = null) + { + if (null === $maxBufferLevel) { + $maxBufferLevel = ob_get_level(); + } + + while (ob_get_level() > $maxBufferLevel) { + ob_end_flush(); + } + } + + /** + * Parse content-range header + * https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16 + * + * @param string $header The Content-Range header to parse. + * @return false|array [unit, first, last, length]; returns false if no + * content range or an invalid content range is provided + */ + protected function parseContentRange($header) + { + if (preg_match('/(?P[\w]+)\s+(?P\d+)-(?P\d+)\/(?P\d+|\*)/', $header, $matches)) { + return [ + $matches['unit'], + (int)$matches['first'], + (int)$matches['last'], + $matches['length'] === '*' ? '*' : (int)$matches['length'], + ]; + } + + return false; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/ResponseTransformer.php b/app/vendor/cakephp/cakephp/src/Http/ResponseTransformer.php new file mode 100644 index 000000000..58363baae --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/ResponseTransformer.php @@ -0,0 +1,275 @@ + $response->getStatusCode(), + 'body' => $body['body'], + ]; + $cake = new CakeResponse($data); + if ($body['file']) { + $cake->file($body['file']); + } + $cookies = static::parseCookies($response->getHeader('Set-Cookie')); + foreach ($cookies as $cookie) { + $cake->cookie($cookie); + } + $headers = static::collapseHeaders($response); + $cake->header($headers); + + if (!empty($headers['Content-Type'])) { + $cake->type($headers['Content-Type']); + } + + return $cake; + } + + /** + * Get the response body from a PSR7 Response. + * + * @param \Psr\Http\Message\ResponseInterface $response The response to convert. + * @return array A hash of 'body' and 'file' + */ + protected static function getBody(PsrResponse $response) + { + $stream = $response->getBody(); + if ($stream->getMetadata('wrapper_type') === 'plainfile') { + return ['body' => '', 'file' => $stream->getMetadata('uri')]; + } + if ($stream->getSize() === 0) { + return ['body' => '', 'file' => false]; + } + $stream->rewind(); + + return ['body' => $stream->getContents(), 'file' => false]; + } + + /** + * Parse the Set-Cookie headers in a PSR7 response + * into the format CakePHP expects. + * + * @param array $cookieHeader A list of Set-Cookie headers. + * @return array Parsed cookie data. + */ + protected static function parseCookies(array $cookieHeader) + { + $cookies = []; + foreach ($cookieHeader as $cookie) { + if (strpos($cookie, '";"') !== false) { + $cookie = str_replace('";"', '{__cookie_replace__}', $cookie); + $parts = preg_split('/\;[ \t]*/', $cookie); + $parts = str_replace('{__cookie_replace__}', '";"', $parts); + } else { + $parts = preg_split('/\;[ \t]*/', $cookie); + } + + list($name, $value) = explode('=', array_shift($parts), 2); + $parsed = ['name' => $name, 'value' => urldecode($value)]; + + foreach ($parts as $part) { + if (strpos($part, '=') !== false) { + list($key, $value) = explode('=', $part); + } else { + $key = $part; + $value = true; + } + + $key = strtolower($key); + if ($key === 'httponly') { + $key = 'httpOnly'; + } + if ($key === 'expires') { + $key = 'expire'; + $value = strtotime($value); + } + if (!isset($parsed[$key])) { + $parsed[$key] = $value; + } + } + $cookies[] = $parsed; + } + + return $cookies; + } + + /** + * Convert a PSR7 Response headers into a flat array + * + * @param \Psr\Http\Message\ResponseInterface $response The response to convert. + * @return array Headers. + */ + protected static function collapseHeaders(PsrResponse $response) + { + $out = []; + foreach ($response->getHeaders() as $name => $value) { + if (count($value) === 1) { + $out[$name] = $value[0]; + } else { + $out[$name] = $value; + } + } + + return $out; + } + + /** + * Convert a CakePHP response into a PSR7 one. + * + * @param \Cake\Http\Response $response The CakePHP response to convert + * @return \Psr\Http\Message\ResponseInterface $response The equivalent PSR7 response. + */ + public static function toPsr(CakeResponse $response) + { + $status = $response->statusCode(); + $headers = $response->header(); + if (!isset($headers['Content-Type'])) { + $headers = static::setContentType($headers, $response); + } + $cookies = $response->cookie(); + if ($cookies) { + $headers['Set-Cookie'] = static::buildCookieHeader($cookies); + } + $stream = static::getStream($response); + + return new DiactorosResponse($stream, $status, $headers); + } + + /** + * Add in the Content-Type header if necessary. + * + * @param array $headers The headers to update + * @param \Cake\Http\Response $response The CakePHP response to convert + * @return array The updated headers. + */ + protected static function setContentType($headers, $response) + { + if (isset($headers['Content-Type'])) { + return $headers; + } + if (in_array($response->statusCode(), [204, 304])) { + return $headers; + } + + $whitelist = [ + 'application/javascript', 'application/json', 'application/xml', 'application/rss+xml' + ]; + + $type = $response->type(); + $charset = $response->charset(); + + $hasCharset = false; + if ($charset && (strpos($type, 'text/') === 0 || in_array($type, $whitelist))) { + $hasCharset = true; + } + + $value = $type; + if ($hasCharset) { + $value = "{$type}; charset={$charset}"; + } + $headers['Content-Type'] = $value; + + return $headers; + } + + /** + * Convert an array of cookies into header lines. + * + * @param array $cookies The cookies to serialize. + * @return array A list of cookie header values. + */ + protected static function buildCookieHeader($cookies) + { + $headers = []; + foreach ($cookies as $cookie) { + $parts = [ + sprintf('%s=%s', urlencode($cookie['name']), urlencode($cookie['value'])) + ]; + if ($cookie['expire']) { + $cookie['expire'] = gmdate('D, d M Y H:i:s T', $cookie['expire']); + } + $attributes = [ + 'expire' => 'Expires=%s', + 'path' => 'Path=%s', + 'domain' => 'Domain=%s', + 'httpOnly' => 'HttpOnly', + 'secure' => 'Secure', + ]; + foreach ($attributes as $key => $attr) { + if ($cookie[$key]) { + $parts[] = sprintf($attr, $cookie[$key]); + } + } + $headers[] = implode('; ', $parts); + } + + return $headers; + } + + /** + * Get the stream for the new response. + * + * @param \Cake\Http\Response $response The cake response to extract the body from. + * @return \Psr\Http\Message\StreamInterface|string The stream. + */ + protected static function getStream($response) + { + $stream = 'php://memory'; + $body = $response->body(); + if (is_string($body) && strlen($body)) { + $stream = new Stream('php://memory', 'wb'); + $stream->write($body); + + return $stream; + } + if (is_callable($body)) { + $stream = new CallbackStream($body); + + return $stream; + } + $file = $response->getFile(); + if ($file) { + $stream = new Stream($file->path, 'rb'); + + return $stream; + } + + return $stream; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Runner.php b/app/vendor/cakephp/cakephp/src/Http/Runner.php new file mode 100644 index 000000000..a6eaa8146 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Runner.php @@ -0,0 +1,71 @@ +middleware = $middleware; + $this->index = 0; + + return $this->__invoke($request, $response); + } + + /** + * @param \Psr\Http\Message\ServerRequestInterface $request The server request + * @param \Psr\Http\Message\ResponseInterface $response The response object + * @return \Psr\Http\Message\ResponseInterface An updated response + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response) + { + $next = $this->middleware->get($this->index); + if ($next) { + $this->index++; + + return $next($request, $response, $this); + } + + // End of the queue + return $response; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Server.php b/app/vendor/cakephp/cakephp/src/Http/Server.php new file mode 100644 index 000000000..5e2691b03 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Server.php @@ -0,0 +1,220 @@ +app = $app; + $this->setRunner(new Runner()); + } + + /** + * Run the request/response through the Application and its middleware. + * + * This will invoke the following methods: + * + * - App->bootstrap() - Perform any bootstrapping logic for your application here. + * - App->middleware() - Attach any application middleware here. + * - Trigger the 'Server.buildMiddleware' event. You can use this to modify the + * from event listeners. + * - Run the middleware queue including the application. + * + * @param \Psr\Http\Message\ServerRequestInterface|null $request The request to use or null. + * @param \Psr\Http\Message\ResponseInterface|null $response The response to use or null. + * @return \Psr\Http\Message\ResponseInterface + * @throws \RuntimeException When the application does not make a response. + */ + public function run(ServerRequestInterface $request = null, ResponseInterface $response = null) + { + $this->bootstrap(); + + $response = $response ?: new Response(); + $request = $request ?: ServerRequestFactory::fromGlobals(); + + $middleware = $this->app->middleware(new MiddlewareQueue()); + if ($this->app instanceof PluginApplicationInterface) { + $middleware = $this->app->pluginMiddleware($middleware); + } + + if (!($middleware instanceof MiddlewareQueue)) { + throw new RuntimeException('The application `middleware` method did not return a middleware queue.'); + } + $this->dispatchEvent('Server.buildMiddleware', ['middleware' => $middleware]); + $middleware->add($this->app); + + $response = $this->runner->run($middleware, $request, $response); + + if (!($response instanceof ResponseInterface)) { + throw new RuntimeException(sprintf( + 'Application did not create a response. Got "%s" instead.', + is_object($response) ? get_class($response) : $response + )); + } + + return $response; + } + + /** + * Application bootstrap wrapper. + * + * Calls `bootstrap()` and `events()` if application implements `EventApplicationInterface`. + * After the application is bootstrapped and events are attached, plugins are bootstrapped + * and have their events attached. + * + * @return void + */ + protected function bootstrap() + { + $this->app->bootstrap(); + + if ($this->app instanceof PluginApplicationInterface) { + $this->app->pluginBootstrap(); + } + } + + /** + * Emit the response using the PHP SAPI. + * + * @param \Psr\Http\Message\ResponseInterface $response The response to emit + * @param \Zend\Diactoros\Response\EmitterInterface|null $emitter The emitter to use. + * When null, a SAPI Stream Emitter will be used. + * @return void + */ + public function emit(ResponseInterface $response, EmitterInterface $emitter = null) + { + if (!$emitter) { + $emitter = new ResponseEmitter(); + } + $emitter->emit($response); + } + + /** + * Get the current application. + * + * @return \Cake\Core\HttpApplicationInterface The application that will be run. + */ + public function getApp() + { + return $this->app; + } + + /** + * Set the runner + * + * @param \Cake\Http\Runner $runner The runner to use. + * @return $this + */ + public function setRunner(Runner $runner) + { + $this->runner = $runner; + + return $this; + } + + /** + * Get the application's event manager or the global one. + * + * @return \Cake\Event\EventManagerInterface + */ + public function getEventManager() + { + if ($this->app instanceof PluginApplicationInterface) { + return $this->app->getEventManager(); + } + + return EventManager::instance(); + } + + /** + * Get/set the application's event manager. + * + * If the application does not support events and this method is used as + * a setter, an exception will be raised. + * + * @param \Cake\Event\EventManager|null $events The event manager to set. + * @return \Cake\Event\EventManager|$this + * @deprecated 3.6.0 Will be removed in 4.0 + */ + public function eventManager(EventManager $events = null) + { + deprecationWarning('eventManager() is deprecated. Use getEventManager()/setEventManager() instead.'); + if ($events === null) { + return $this->getEventManager(); + } + + return $this->setEventManager($events); + } + + /** + * Get/set the application's event manager. + * + * If the application does not support events and this method is used as + * a setter, an exception will be raised. + * + * @param \Cake\Event\EventManager $events The event manager to set. + * @return $this + */ + public function setEventManager(EventManager $events) + { + if ($this->app instanceof PluginApplicationInterface) { + $this->app->setEventManager($events); + + return $this; + } + + throw new InvalidArgumentException('Cannot set the event manager, the application does not support events.'); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/ServerRequest.php b/app/vendor/cakephp/cakephp/src/Http/ServerRequest.php new file mode 100644 index 000000000..d2057227e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/ServerRequest.php @@ -0,0 +1,2450 @@ + null, + 'controller' => null, + 'action' => null, + '_ext' => null, + 'pass' => [] + ]; + + /** + * Array of POST data. Will contain form data as well as uploaded files. + * In PUT/PATCH/DELETE requests this property will contain the form-urlencoded + * data. + * + * @var null|array|object + * @deprecated 3.4.0 This public property will be removed in 4.0.0. Use getData() instead. + */ + protected $data = []; + + /** + * Array of query string arguments + * + * @var array + * @deprecated 3.4.0 This public property will be removed in 4.0.0. Use getQuery() or getQueryParams() instead. + */ + protected $query = []; + + /** + * Array of cookie data. + * + * @var array + * @deprecated 3.4.0 This public property will be removed in 4.0.0. Use getCookie() instead. + */ + protected $cookies = []; + + /** + * Array of environment data. + * + * @var array + */ + protected $_environment = []; + + /** + * The URL string used for the request. + * + * @var string + * @deprecated 3.6.0 This public property will be removed in 4.0.0. Use getRequestTarget() instead. + */ + protected $url; + + /** + * Base URL path. + * + * @var string + * @deprecated 3.4.0 This public property will be removed in 4.0.0. Use getAttribute('base') instead. + */ + protected $base; + + /** + * webroot path segment for the request. + * + * @var string + * @deprecated 3.4.0 This public property will be removed in 4.0.0. Use getAttribute('webroot') instead. + */ + protected $webroot = '/'; + + /** + * The full address to the current request + * + * @var string + * @deprecated 3.4.0 This public property will be removed in 4.0.0. Use getAttribute('here') or getUri()->getPath() instead. + */ + protected $here; + + /** + * Whether or not to trust HTTP_X headers set by most load balancers. + * Only set to true if your application runs behind load balancers/proxies + * that you control. + * + * @var bool + */ + public $trustProxy = false; + + /** + * Contents of php://input + * + * @var string + */ + protected $_input; + + /** + * The built in detectors used with `is()` can be modified with `addDetector()`. + * + * There are several ways to specify a detector, see \Cake\Http\ServerRequest::addDetector() for the + * various formats and ways to define detectors. + * + * @var array + */ + protected static $_detectors = [ + 'get' => ['env' => 'REQUEST_METHOD', 'value' => 'GET'], + 'post' => ['env' => 'REQUEST_METHOD', 'value' => 'POST'], + 'put' => ['env' => 'REQUEST_METHOD', 'value' => 'PUT'], + 'patch' => ['env' => 'REQUEST_METHOD', 'value' => 'PATCH'], + 'delete' => ['env' => 'REQUEST_METHOD', 'value' => 'DELETE'], + 'head' => ['env' => 'REQUEST_METHOD', 'value' => 'HEAD'], + 'options' => ['env' => 'REQUEST_METHOD', 'value' => 'OPTIONS'], + 'ssl' => ['env' => 'HTTPS', 'options' => [1, 'on']], + 'ajax' => ['env' => 'HTTP_X_REQUESTED_WITH', 'value' => 'XMLHttpRequest'], + 'flash' => ['env' => 'HTTP_USER_AGENT', 'pattern' => '/^(Shockwave|Adobe) Flash/'], + 'requested' => ['param' => 'requested', 'value' => 1], + 'json' => ['accept' => ['application/json'], 'param' => '_ext', 'value' => 'json'], + 'xml' => ['accept' => ['application/xml', 'text/xml'], 'param' => '_ext', 'value' => 'xml'], + ]; + + /** + * Instance cache for results of is(something) calls + * + * @var array + */ + protected $_detectorCache = []; + + /** + * Request body stream. Contains php://input unless `input` constructor option is used. + * + * @var \Psr\Http\Message\StreamInterface + */ + protected $stream; + + /** + * Uri instance + * + * @var \Psr\Http\Message\UriInterface + */ + protected $uri; + + /** + * Instance of a Session object relative to this request + * + * @var \Cake\Http\Session + */ + protected $session; + + /** + * Store the additional attributes attached to the request. + * + * @var array + */ + protected $attributes = []; + + /** + * A list of propertes that emulated by the PSR7 attribute methods. + * + * @var array + */ + protected $emulatedAttributes = ['session', 'webroot', 'base', 'params', 'here']; + + /** + * Array of Psr\Http\Message\UploadedFileInterface objects. + * + * @var array + */ + protected $uploadedFiles = []; + + /** + * The HTTP protocol version used. + * + * @var string|null + */ + protected $protocol; + + /** + * The request target if overridden + * + * @var string|null + */ + protected $requestTarget; + + /** + * List of deprecated properties that have backwards + * compatibility offered through magic methods. + * + * @var array + */ + private $deprecatedProperties = [ + 'data' => ['get' => 'getData()', 'set' => 'withData()'], + 'query' => ['get' => 'getQuery()', 'set' => 'withQueryParams()'], + 'params' => ['get' => 'getParam()', 'set' => 'withParam()'], + 'cookies' => ['get' => 'getCookie()', 'set' => 'withCookieParams()'], + 'url' => ['get' => 'getPath()', 'set' => 'withRequestTarget()'], + 'base' => ['get' => 'getAttribute("base")', 'set' => 'withAttribute("base")'], + 'webroot' => ['get' => 'getAttribute("webroot")', 'set' => 'withAttribute("webroot")'], + 'here' => ['get' => 'getAttribute("here")', 'set' => 'withAttribute("here")'], + ]; + + /** + * Wrapper method to create a new request from PHP superglobals. + * + * Uses the $_GET, $_POST, $_FILES, $_COOKIE, $_SERVER, $_ENV and php://input data to construct + * the request. + * + * @return self + * @deprecated 3.4.0 Use `Cake\Http\ServerRequestFactory` instead. + */ + public static function createFromGlobals() + { + deprecationWarning( + 'ServerRequest::createFromGlobals() is deprecated. ' . + 'Use `Cake\Http\ServerRequestFactory` instead.' + ); + + return ServerRequestFactory::fromGlobals(); + } + + /** + * Create a new request object. + * + * You can supply the data as either an array or as a string. If you use + * a string you can only supply the URL for the request. Using an array will + * let you provide the following keys: + * + * - `post` POST data or non query string data + * - `query` Additional data from the query string. + * - `files` Uploaded file data formatted like $_FILES. + * - `cookies` Cookies for this request. + * - `environment` $_SERVER and $_ENV data. + * - ~~`url`~~ The URL without the base path for the request. This option is deprecated and will be removed in 4.0.0 + * - `uri` The PSR7 UriInterface object. If null, one will be created. + * - `base` The base URL for the request. + * - `webroot` The webroot directory for the request. + * - `input` The data that would come from php://input this is useful for simulating + * requests with put, patch or delete data. + * - `session` An instance of a Session object + * + * @param string|array $config An array of request data to create a request with. + * The string version of this argument is *deprecated* and will be removed in 4.0.0 + */ + public function __construct($config = []) + { + if (is_string($config)) { + $config = ['url' => $config]; + } + $config += [ + 'params' => $this->params, + 'query' => [], + 'post' => [], + 'files' => [], + 'cookies' => [], + 'environment' => [], + 'url' => '', + 'uri' => null, + 'base' => '', + 'webroot' => '', + 'input' => null, + ]; + + $this->_setConfig($config); + } + + /** + * Process the config/settings data into properties. + * + * @param array $config The config data to use. + * @return void + */ + protected function _setConfig($config) + { + if (!empty($config['url']) && $config['url'][0] === '/') { + $config['url'] = substr($config['url'], 1); + } + + if (empty($config['session'])) { + $config['session'] = new Session([ + 'cookiePath' => $config['base'] + ]); + } + + $this->_environment = $config['environment']; + $this->cookies = $config['cookies']; + + if (isset($config['uri']) && $config['uri'] instanceof UriInterface) { + $uri = $config['uri']; + } else { + $uri = ServerRequestFactory::createUri($config['environment']); + } + + // Extract a query string from config[url] if present. + // This is required for backwards compatibility and keeping + // UriInterface implementations happy. + $querystr = ''; + if (strpos($config['url'], '?') !== false) { + list($config['url'], $querystr) = explode('?', $config['url']); + } + if (strlen($config['url'])) { + $uri = $uri->withPath('/' . $config['url']); + } + if (strlen($querystr)) { + $uri = $uri->withQuery($querystr); + } + + $this->uri = $uri; + $this->base = $config['base']; + $this->webroot = $config['webroot']; + + $this->url = substr($uri->getPath(), 1); + $this->here = $this->base . '/' . $this->url; + + if (isset($config['input'])) { + $stream = new Stream('php://memory', 'rw'); + $stream->write($config['input']); + $stream->rewind(); + } else { + $stream = new PhpInputStream(); + } + $this->stream = $stream; + + $config['post'] = $this->_processPost($config['post']); + $this->data = $this->_processFiles($config['post'], $config['files']); + $this->query = $this->_processGet($config['query'], $querystr); + $this->params = $config['params']; + $this->session = $config['session']; + } + + /** + * Sets the REQUEST_METHOD environment variable based on the simulated _method + * HTTP override value. The 'ORIGINAL_REQUEST_METHOD' is also preserved, if you + * want the read the non-simulated HTTP method the client used. + * + * @param array $data Array of post data. + * @return array + */ + protected function _processPost($data) + { + $method = $this->getEnv('REQUEST_METHOD'); + $override = false; + + if (in_array($method, ['PUT', 'DELETE', 'PATCH']) && + strpos($this->contentType(), 'application/x-www-form-urlencoded') === 0 + ) { + $data = $this->input(); + parse_str($data, $data); + } + if ($this->hasHeader('X-Http-Method-Override')) { + $data['_method'] = $this->getHeaderLine('X-Http-Method-Override'); + $override = true; + } + $this->_environment['ORIGINAL_REQUEST_METHOD'] = $method; + if (isset($data['_method'])) { + $this->_environment['REQUEST_METHOD'] = $data['_method']; + unset($data['_method']); + $override = true; + } + + if ($override && !in_array($this->_environment['REQUEST_METHOD'], ['PUT', 'POST', 'DELETE', 'PATCH'])) { + $data = []; + } + + return $data; + } + + /** + * Process the GET parameters and move things into the object. + * + * @param array $query The array to which the parsed keys/values are being added. + * @param string $queryString A query string from the URL if provided + * @return array An array containing the parsed query string as keys/values. + */ + protected function _processGet($query, $queryString = '') + { + $unsetUrl = '/' . str_replace(['.', ' '], '_', urldecode($this->url)); + unset($query[$unsetUrl], $query[$this->base . $unsetUrl]); + if (strlen($queryString)) { + parse_str($queryString, $queryArgs); + $query += $queryArgs; + } + + return $query; + } + + /** + * Process uploaded files and move things onto the post data. + * + * @param array $post Post data to merge files onto. + * @param array $files Uploaded files to merge in. + * @return array merged post + file data. + */ + protected function _processFiles($post, $files) + { + if (!is_array($files)) { + return $post; + } + $fileData = []; + foreach ($files as $key => $value) { + if ($value instanceof UploadedFileInterface) { + $fileData[$key] = $value; + continue; + } + + if (is_array($value) && isset($value['tmp_name'])) { + $fileData[$key] = $this->_createUploadedFile($value); + continue; + } + + throw new InvalidArgumentException(sprintf( + 'Invalid value in FILES "%s"', + json_encode($value) + )); + } + $this->uploadedFiles = $fileData; + + // Make a flat map that can be inserted into $post for BC. + $fileMap = Hash::flatten($fileData); + foreach ($fileMap as $key => $file) { + $error = $file->getError(); + $tmpName = ''; + if ($error === UPLOAD_ERR_OK) { + $tmpName = $file->getStream()->getMetadata('uri'); + } + $post = Hash::insert($post, $key, [ + 'tmp_name' => $tmpName, + 'error' => $error, + 'name' => $file->getClientFilename(), + 'type' => $file->getClientMediaType(), + 'size' => $file->getSize(), + ]); + } + + return $post; + } + + /** + * Create an UploadedFile instance from a $_FILES array. + * + * If the value represents an array of values, this method will + * recursively process the data. + * + * @param array $value $_FILES struct + * @return array|UploadedFileInterface + */ + protected function _createUploadedFile(array $value) + { + if (is_array($value['tmp_name'])) { + return $this->_normalizeNestedFiles($value); + } + + return new UploadedFile( + $value['tmp_name'], + $value['size'], + $value['error'], + $value['name'], + $value['type'] + ); + } + + /** + * Normalize an array of file specifications. + * + * Loops through all nested files and returns a normalized array of + * UploadedFileInterface instances. + * + * @param array $files The file data to normalize & convert. + * @return array An array of UploadedFileInterface objects. + */ + protected function _normalizeNestedFiles(array $files = []) + { + $normalizedFiles = []; + foreach (array_keys($files['tmp_name']) as $key) { + $spec = [ + 'tmp_name' => $files['tmp_name'][$key], + 'size' => $files['size'][$key], + 'error' => $files['error'][$key], + 'name' => $files['name'][$key], + 'type' => $files['type'][$key], + ]; + $normalizedFiles[$key] = $this->_createUploadedFile($spec); + } + + return $normalizedFiles; + } + + /** + * Get the content type used in this request. + * + * @return string + */ + public function contentType() + { + $type = $this->getEnv('CONTENT_TYPE'); + if ($type) { + return $type; + } + + return $this->getEnv('HTTP_CONTENT_TYPE'); + } + + /** + * Returns the instance of the Session object for this request + * + * @return \Cake\Http\Session + */ + public function getSession() + { + return $this->session; + } + + /** + * Returns the instance of the Session object for this request + * + * If a session object is passed as first argument it will be set as + * the session to use for this request + * + * @deprecated 3.5.0 Use getSession() instead. The setter part will be removed. + * @param \Cake\Http\Session|null $session the session object to use + * @return \Cake\Http\Session + */ + public function session(Session $session = null) + { + deprecationWarning( + 'ServerRequest::session() is deprecated. ' . + 'Use getSession() instead. The setter part will be removed.' + ); + + if ($session === null) { + return $this->session; + } + + return $this->session = $session; + } + + /** + * Get the IP the client is using, or says they are using. + * + * @return string The client IP. + */ + public function clientIp() + { + if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_FOR')) { + $addresses = explode(',', $this->getEnv('HTTP_X_FORWARDED_FOR')); + $ipaddr = end($addresses); + } elseif ($this->trustProxy && $this->getEnv('HTTP_CLIENT_IP')) { + $ipaddr = $this->getEnv('HTTP_CLIENT_IP'); + } else { + $ipaddr = $this->getEnv('REMOTE_ADDR'); + } + + return trim($ipaddr); + } + + /** + * Returns the referer that referred this request. + * + * @param bool $local Attempt to return a local address. + * Local addresses do not contain hostnames. + * @return string The referring address for this request. + */ + public function referer($local = false) + { + $ref = $this->getEnv('HTTP_REFERER'); + + $base = Configure::read('App.fullBaseUrl') . $this->webroot; + if (!empty($ref) && !empty($base)) { + if ($local && strpos($ref, $base) === 0) { + $ref = substr($ref, strlen($base)); + if (!strlen($ref) || strpos($ref, '//') === 0) { + $ref = '/'; + } + if ($ref[0] !== '/') { + $ref = '/' . $ref; + } + + return $ref; + } + if (!$local) { + return $ref; + } + } + + return '/'; + } + + /** + * Missing method handler, handles wrapping older style isAjax() type methods + * + * @param string $name The method called + * @param array $params Array of parameters for the method call + * @return mixed + * @throws \BadMethodCallException when an invalid method is called. + */ + public function __call($name, $params) + { + if (strpos($name, 'is') === 0) { + $type = strtolower(substr($name, 2)); + + array_unshift($params, $type); + + return $this->is(...$params); + } + throw new BadMethodCallException(sprintf('Method %s does not exist', $name)); + } + + /** + * Magic set method allows backward compatibility for former public properties + * + * + * @param string $name The property being accessed. + * @param mixed $value The property value. + * @return mixed Either the value of the parameter or null. + * @deprecated 3.6.0 Public properties will be removed in 4.0.0. + * Use appropriate setters instead. + */ + public function __set($name, $value) + { + if (isset($this->deprecatedProperties[$name])) { + $method = $this->deprecatedProperties[$name]['set']; + deprecationWarning( + "Setting {$name} as a property will be removed in 4.0.0. " . + "Use {$method} instead." + ); + + return $this->{$name} = $value; + } + throw new BadMethodCallException("Cannot set {$name} it is not a known property."); + } + + /** + * Magic get method allows access to parsed routing parameters directly on the object. + * + * Allows access to `$this->params['controller']` via `$this->controller` + * + * @param string $name The property being accessed. + * @return mixed Either the value of the parameter or null. + * @deprecated 3.4.0 Accessing routing parameters through __get will removed in 4.0.0. + * Use getParam() instead. + */ + public function &__get($name) + { + if (isset($this->deprecatedProperties[$name])) { + $method = $this->deprecatedProperties[$name]['get']; + deprecationWarning( + "Accessing `{$name}` as a property will be removed in 4.0.0. " . + "Use request->{$method} instead." + ); + + return $this->{$name}; + } + + deprecationWarning(sprintf( + 'Accessing routing parameters through `%s` will removed in 4.0.0. ' . + 'Use `getParam()` instead.', + $name + )); + + if (isset($this->params[$name])) { + return $this->params[$name]; + } + $value = null; + + return $value; + } + + /** + * Magic isset method allows isset/empty checks + * on routing parameters. + * + * @param string $name The property being accessed. + * @return bool Existence + * @deprecated 3.4.0 Accessing routing parameters through __isset will removed in 4.0.0. + * Use getParam() instead. + */ + public function __isset($name) + { + if (isset($this->deprecatedProperties[$name])) { + $method = $this->deprecatedProperties[$name]['get']; + deprecationWarning( + "Accessing {$name} as a property will be removed in 4.0.0. " . + "Use {$method} instead." + ); + + return isset($this->{$name}); + } + + deprecationWarning( + 'Accessing routing parameters through __isset will removed in 4.0.0. ' . + 'Use getParam() instead.' + ); + + return isset($this->params[$name]); + } + + /** + * Check whether or not a Request is a certain type. + * + * Uses the built in detection rules as well as additional rules + * defined with Cake\Http\ServerRequest::addDetector(). Any detector can be called + * as `is($type)` or `is$Type()`. + * + * @param string|array $type The type of request you want to check. If an array + * this method will return true if the request matches any type. + * @param array ...$args List of arguments + * @return bool Whether or not the request is the type you are checking. + */ + public function is($type, ...$args) + { + if (is_array($type)) { + $result = array_map([$this, 'is'], $type); + + return count(array_filter($result)) > 0; + } + + $type = strtolower($type); + if (!isset(static::$_detectors[$type])) { + return false; + } + if ($args) { + return $this->_is($type, $args); + } + if (!isset($this->_detectorCache[$type])) { + $this->_detectorCache[$type] = $this->_is($type, $args); + } + + return $this->_detectorCache[$type]; + } + + /** + * Clears the instance detector cache, used by the is() function + * + * @return void + */ + public function clearDetectorCache() + { + $this->_detectorCache = []; + } + + /** + * Worker for the public is() function + * + * @param string $type The type of request you want to check. + * @param array $args Array of custom detector arguments. + * @return bool Whether or not the request is the type you are checking. + */ + protected function _is($type, $args) + { + $detect = static::$_detectors[$type]; + if (is_callable($detect)) { + array_unshift($args, $this); + + return $detect(...$args); + } + if (isset($detect['env']) && $this->_environmentDetector($detect)) { + return true; + } + if (isset($detect['header']) && $this->_headerDetector($detect)) { + return true; + } + if (isset($detect['accept']) && $this->_acceptHeaderDetector($detect)) { + return true; + } + if (isset($detect['param']) && $this->_paramDetector($detect)) { + return true; + } + + return false; + } + + /** + * Detects if a specific accept header is present. + * + * @param array $detect Detector options array. + * @return bool Whether or not the request is the type you are checking. + */ + protected function _acceptHeaderDetector($detect) + { + $acceptHeaders = explode(',', $this->getEnv('HTTP_ACCEPT')); + foreach ($detect['accept'] as $header) { + if (in_array($header, $acceptHeaders)) { + return true; + } + } + + return false; + } + + /** + * Detects if a specific header is present. + * + * @param array $detect Detector options array. + * @return bool Whether or not the request is the type you are checking. + */ + protected function _headerDetector($detect) + { + foreach ($detect['header'] as $header => $value) { + $header = $this->getEnv('http_' . $header); + if ($header !== null) { + if (!is_string($value) && !is_bool($value) && is_callable($value)) { + return call_user_func($value, $header); + } + + return ($header === $value); + } + } + + return false; + } + + /** + * Detects if a specific request parameter is present. + * + * @param array $detect Detector options array. + * @return bool Whether or not the request is the type you are checking. + */ + protected function _paramDetector($detect) + { + $key = $detect['param']; + if (isset($detect['value'])) { + $value = $detect['value']; + + return isset($this->params[$key]) ? $this->params[$key] == $value : false; + } + if (isset($detect['options'])) { + return isset($this->params[$key]) ? in_array($this->params[$key], $detect['options']) : false; + } + + return false; + } + + /** + * Detects if a specific environment variable is present. + * + * @param array $detect Detector options array. + * @return bool Whether or not the request is the type you are checking. + */ + protected function _environmentDetector($detect) + { + if (isset($detect['env'])) { + if (isset($detect['value'])) { + return $this->getEnv($detect['env']) == $detect['value']; + } + if (isset($detect['pattern'])) { + return (bool)preg_match($detect['pattern'], $this->getEnv($detect['env'])); + } + if (isset($detect['options'])) { + $pattern = '/' . implode('|', $detect['options']) . '/i'; + + return (bool)preg_match($pattern, $this->getEnv($detect['env'])); + } + } + + return false; + } + + /** + * Check that a request matches all the given types. + * + * Allows you to test multiple types and union the results. + * See Request::is() for how to add additional types and the + * built-in types. + * + * @param array $types The types to check. + * @return bool Success. + * @see \Cake\Http\ServerRequest::is() + */ + public function isAll(array $types) + { + $result = array_filter(array_map([$this, 'is'], $types)); + + return count($result) === count($types); + } + + /** + * Add a new detector to the list of detectors that a request can use. + * There are several different formats and types of detectors that can be set. + * + * ### Callback detectors + * + * Callback detectors allow you to provide a callable to handle the check. + * The callback will receive the request object as its only parameter. + * + * ``` + * addDetector('custom', function ($request) { //Return a boolean }); + * addDetector('custom', ['SomeClass', 'somemethod']); + * ``` + * + * ### Environment value comparison + * + * An environment value comparison, compares a value fetched from `env()` to a known value + * the environment value is equality checked against the provided value. + * + * e.g `addDetector('post', ['env' => 'REQUEST_METHOD', 'value' => 'POST'])` + * + * ### Pattern value comparison + * + * Pattern value comparison allows you to compare a value fetched from `env()` to a regular expression. + * + * ``` + * addDetector('iphone', ['env' => 'HTTP_USER_AGENT', 'pattern' => '/iPhone/i']); + * ``` + * + * ### Option based comparison + * + * Option based comparisons use a list of options to create a regular expression. Subsequent calls + * to add an already defined options detector will merge the options. + * + * ``` + * addDetector('mobile', ['env' => 'HTTP_USER_AGENT', 'options' => ['Fennec']]); + * ``` + * + * ### Request parameter detectors + * + * Allows for custom detectors on the request parameters. + * + * e.g `addDetector('requested', ['param' => 'requested', 'value' => 1]` + * + * You can also make parameter detectors that accept multiple values + * using the `options` key. This is useful when you want to check + * if a request parameter is in a list of options. + * + * `addDetector('extension', ['param' => 'ext', 'options' => ['pdf', 'csv']]` + * + * @param string $name The name of the detector. + * @param callable|array $callable A callable or options array for the detector definition. + * @return void + */ + public static function addDetector($name, $callable) + { + $name = strtolower($name); + if (is_callable($callable)) { + static::$_detectors[$name] = $callable; + + return; + } + if (isset(static::$_detectors[$name], $callable['options'])) { + $callable = Hash::merge(static::$_detectors[$name], $callable); + } + static::$_detectors[$name] = $callable; + } + + /** + * Add parameters to the request's parsed parameter set. This will overwrite any existing parameters. + * This modifies the parameters available through `$request->getParam()`. + * + * @param array $params Array of parameters to merge in + * @return $this The current object, you can chain this method. + * @deprecated 3.6.0 ServerRequest::addParams() is deprecated. Use `withParam()` or + * `withAttribute('params')` instead. + */ + public function addParams(array $params) + { + deprecationWarning( + 'ServerRequest::addParams() is deprecated. ' . + 'Use `withParam()` or `withAttribute("params", $params)` instead.' + ); + $this->params = array_merge($this->params, $params); + + return $this; + } + + /** + * Add paths to the requests' paths vars. This will overwrite any existing paths. + * Provides an easy way to modify, here, webroot and base. + * + * @param array $paths Array of paths to merge in + * @return $this The current object, you can chain this method. + * @deprecated 3.6.0 Mutating a request in place is deprecated. Use `withAttribute()` to modify paths instead. + */ + public function addPaths(array $paths) + { + deprecationWarning( + 'ServerRequest::addPaths() is deprecated. ' . + 'Use `withAttribute($key, $value)` instead.' + ); + foreach (['webroot', 'here', 'base'] as $element) { + if (isset($paths[$element])) { + $this->{$element} = $paths[$element]; + } + } + + return $this; + } + + /** + * Get the value of the current requests URL. Will include the query string arguments. + * + * @param bool $base Include the base path, set to false to trim the base path off. + * @return string The current request URL including query string args. + * @deprecated 3.4.0 This method will be removed in 4.0.0. You should use getRequestTarget() instead. + */ + public function here($base = true) + { + deprecationWarning( + 'ServerRequest::here() will be removed in 4.0.0. You should use getRequestTarget() instead.' + ); + + $url = $this->here; + if (!empty($this->query)) { + $url .= '?' . http_build_query($this->query, null, '&'); + } + if (!$base) { + $url = preg_replace('/^' . preg_quote($this->base, '/') . '/', '', $url, 1); + } + + return $url; + } + + /** + * Normalize a header name into the SERVER version. + * + * @param string $name The header name. + * @return string The normalized header name. + */ + protected function normalizeHeaderName($name) + { + $name = str_replace('-', '_', strtoupper($name)); + if (!in_array($name, ['CONTENT_LENGTH', 'CONTENT_TYPE'])) { + $name = 'HTTP_' . $name; + } + + return $name; + } + + /** + * Read an HTTP header from the Request information. + * + * If the header is not defined in the request, this method + * will fallback to reading data from $_SERVER and $_ENV. + * This fallback behavior is deprecated, and will be removed in 4.0.0 + * + * @param string $name Name of the header you want. + * @return string|null Either null on no header being set or the value of the header. + * @deprecated 4.0.0 The automatic fallback to env() will be removed in 4.0.0, see getHeader() + */ + public function header($name) + { + deprecationWarning( + 'ServerRequest::header() is deprecated. ' . + 'The automatic fallback to env() will be removed in 4.0.0, see getHeader()' + ); + + $name = $this->normalizeHeaderName($name); + + return $this->getEnv($name); + } + + /** + * Get all headers in the request. + * + * Returns an associative array where the header names are + * the keys and the values are a list of header values. + * + * While header names are not case-sensitive, getHeaders() will normalize + * the headers. + * + * @return array An associative array of headers and their values. + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function getHeaders() + { + $headers = []; + foreach ($this->_environment as $key => $value) { + $name = null; + if (strpos($key, 'HTTP_') === 0) { + $name = substr($key, 5); + } + if (strpos($key, 'CONTENT_') === 0) { + $name = $key; + } + if ($name !== null) { + $name = str_replace('_', ' ', strtolower($name)); + $name = str_replace(' ', '-', ucwords($name)); + $headers[$name] = (array)$value; + } + } + + return $headers; + } + + /** + * Check if a header is set in the request. + * + * @param string $name The header you want to get (case-insensitive) + * @return bool Whether or not the header is defined. + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function hasHeader($name) + { + $name = $this->normalizeHeaderName($name); + + return isset($this->_environment[$name]); + } + + /** + * Get a single header from the request. + * + * Return the header value as an array. If the header + * is not present an empty array will be returned. + * + * @param string $name The header you want to get (case-insensitive) + * @return array An associative array of headers and their values. + * If the header doesn't exist, an empty array will be returned. + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function getHeader($name) + { + $name = $this->normalizeHeaderName($name); + if (isset($this->_environment[$name])) { + return (array)$this->_environment[$name]; + } + + return []; + } + + /** + * Get a single header as a string from the request. + * + * @param string $name The header you want to get (case-insensitive) + * @return string Header values collapsed into a comma separated string. + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function getHeaderLine($name) + { + $value = $this->getHeader($name); + + return implode(', ', $value); + } + + /** + * Get a modified request with the provided header. + * + * @param string $name The header name. + * @param string|array $value The header value + * @return static + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function withHeader($name, $value) + { + $new = clone $this; + $name = $this->normalizeHeaderName($name); + $new->_environment[$name] = $value; + + return $new; + } + + /** + * Get a modified request with the provided header. + * + * Existing header values will be retained. The provided value + * will be appended into the existing values. + * + * @param string $name The header name. + * @param string|array $value The header value + * @return static + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function withAddedHeader($name, $value) + { + $new = clone $this; + $name = $this->normalizeHeaderName($name); + $existing = []; + if (isset($new->_environment[$name])) { + $existing = (array)$new->_environment[$name]; + } + $existing = array_merge($existing, (array)$value); + $new->_environment[$name] = $existing; + + return $new; + } + + /** + * Get a modified request without a provided header. + * + * @param string $name The header name to remove. + * @return static + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function withoutHeader($name) + { + $new = clone $this; + $name = $this->normalizeHeaderName($name); + unset($new->_environment[$name]); + + return $new; + } + + /** + * Get the HTTP method used for this request. + * + * @return string The name of the HTTP method used. + * @deprecated 3.4.0 This method will be removed in 4.0.0. Use getMethod() instead. + */ + public function method() + { + deprecationWarning( + 'ServerRequest::method() is deprecated. ' . + 'This method will be removed in 4.0.0. Use getMethod() instead.' + ); + + return $this->getEnv('REQUEST_METHOD'); + } + + /** + * Get the HTTP method used for this request. + * There are a few ways to specify a method. + * + * - If your client supports it you can use native HTTP methods. + * - You can set the HTTP-X-Method-Override header. + * - You can submit an input with the name `_method` + * + * Any of these 3 approaches can be used to set the HTTP method used + * by CakePHP internally, and will effect the result of this method. + * + * @return string The name of the HTTP method used. + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function getMethod() + { + return $this->getEnv('REQUEST_METHOD'); + } + + /** + * Update the request method and get a new instance. + * + * @param string $method The HTTP method to use. + * @return static A new instance with the updated method. + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function withMethod($method) + { + $new = clone $this; + + if (!is_string($method) || + !preg_match('/^[!#$%&\'*+.^_`\|~0-9a-z-]+$/i', $method) + ) { + throw new InvalidArgumentException(sprintf( + 'Unsupported HTTP method "%s" provided', + $method + )); + } + $new->_environment['REQUEST_METHOD'] = $method; + + return $new; + } + + /** + * Get all the server environment parameters. + * + * Read all of the 'environment' or 'server' data that was + * used to create this request. + * + * @return array + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function getServerParams() + { + return $this->_environment; + } + + /** + * Get all the query parameters in accordance to the PSR-7 specifications. To read specific query values + * use the alternative getQuery() method. + * + * @return array + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function getQueryParams() + { + return $this->query; + } + + /** + * Update the query string data and get a new instance. + * + * @param array $query The query string data to use + * @return static A new instance with the updated query string data. + * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface. + */ + public function withQueryParams(array $query) + { + $new = clone $this; + $new->query = $query; + + return $new; + } + + /** + * Get the host that the request was handled on. + * + * @return string + */ + public function host() + { + if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_HOST')) { + return $this->getEnv('HTTP_X_FORWARDED_HOST'); + } + + return $this->getEnv('HTTP_HOST'); + } + + /** + * Get the port the request was handled on. + * + * @return string + */ + public function port() + { + if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_PORT')) { + return $this->getEnv('HTTP_X_FORWARDED_PORT'); + } + + return $this->getEnv('SERVER_PORT'); + } + + /** + * Get the current url scheme used for the request. + * + * e.g. 'http', or 'https' + * + * @return string The scheme used for the request. + */ + public function scheme() + { + if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_PROTO')) { + return $this->getEnv('HTTP_X_FORWARDED_PROTO'); + } + + return $this->getEnv('HTTPS') ? 'https' : 'http'; + } + + /** + * Get the domain name and include $tldLength segments of the tld. + * + * @param int $tldLength Number of segments your tld contains. For example: `example.com` contains 1 tld. + * While `example.co.uk` contains 2. + * @return string Domain name without subdomains. + */ + public function domain($tldLength = 1) + { + $segments = explode('.', $this->host()); + $domain = array_slice($segments, -1 * ($tldLength + 1)); + + return implode('.', $domain); + } + + /** + * Get the subdomains for a host. + * + * @param int $tldLength Number of segments your tld contains. For example: `example.com` contains 1 tld. + * While `example.co.uk` contains 2. + * @return array An array of subdomains. + */ + public function subdomains($tldLength = 1) + { + $segments = explode('.', $this->host()); + + return array_slice($segments, 0, -1 * ($tldLength + 1)); + } + + /** + * Find out which content types the client accepts or check if they accept a + * particular type of content. + * + * #### Get all types: + * + * ``` + * $this->request->accepts(); + * ``` + * + * #### Check for a single type: + * + * ``` + * $this->request->accepts('application/json'); + * ``` + * + * This method will order the returned content types by the preference values indicated + * by the client. + * + * @param string|null $type The content type to check for. Leave null to get all types a client accepts. + * @return array|bool Either an array of all the types the client accepts or a boolean if they accept the + * provided type. + */ + public function accepts($type = null) + { + $raw = $this->parseAccept(); + $accept = []; + foreach ($raw as $types) { + $accept = array_merge($accept, $types); + } + if ($type === null) { + return $accept; + } + + return in_array($type, $accept); + } + + /** + * Parse the HTTP_ACCEPT header and return a sorted array with content types + * as the keys, and pref values as the values. + * + * Generally you want to use Cake\Http\ServerRequest::accept() to get a simple list + * of the accepted content types. + * + * @return array An array of prefValue => [content/types] + */ + public function parseAccept() + { + return $this->_parseAcceptWithQualifier($this->getHeaderLine('Accept')); + } + + /** + * Get the languages accepted by the client, or check if a specific language is accepted. + * + * Get the list of accepted languages: + * + * ``` \Cake\Http\ServerRequest::acceptLanguage(); ``` + * + * Check if a specific language is accepted: + * + * ``` \Cake\Http\ServerRequest::acceptLanguage('es-es'); ``` + * + * @param string|null $language The language to test. + * @return array|bool If a $language is provided, a boolean. Otherwise the array of accepted languages. + */ + public function acceptLanguage($language = null) + { + $raw = $this->_parseAcceptWithQualifier($this->getHeaderLine('Accept-Language')); + $accept = []; + foreach ($raw as $languages) { + foreach ($languages as &$lang) { + if (strpos($lang, '_')) { + $lang = str_replace('_', '-', $lang); + } + $lang = strtolower($lang); + } + $accept = array_merge($accept, $languages); + } + if ($language === null) { + return $accept; + } + + return in_array(strtolower($language), $accept); + } + + /** + * Parse Accept* headers with qualifier options. + * + * Only qualifiers will be extracted, any other accept extensions will be + * discarded as they are not frequently used. + * + * @param string $header Header to parse. + * @return array + */ + protected function _parseAcceptWithQualifier($header) + { + $accept = []; + $header = explode(',', $header); + foreach (array_filter($header) as $value) { + $prefValue = '1.0'; + $value = trim($value); + + $semiPos = strpos($value, ';'); + if ($semiPos !== false) { + $params = explode(';', $value); + $value = trim($params[0]); + foreach ($params as $param) { + $qPos = strpos($param, 'q='); + if ($qPos !== false) { + $prefValue = substr($param, $qPos + 2); + } + } + } + + if (!isset($accept[$prefValue])) { + $accept[$prefValue] = []; + } + if ($prefValue) { + $accept[$prefValue][] = $value; + } + } + krsort($accept); + + return $accept; + } + + /** + * Provides a read accessor for `$this->query`. + * Allows you to use a `Hash::get()` compatible syntax for reading post data. + * + * @param string|null $name Query string variable name or null to read all. + * @return string|array|null The value being read + * @deprecated 3.4.0 Use getQuery() or the PSR-7 getQueryParams() and withQueryParams() methods instead. + */ + public function query($name = null) + { + deprecationWarning( + 'ServerRequest::query() is deprecated. ' . + 'Use getQuery() or the PSR-7 getQueryParams() and withQueryParams() methods instead.' + ); + + if ($name === null) { + return $this->query; + } + + return $this->getQuery($name); + } + + /** + * Read a specific query value or dotted path. + * + * Developers are encouraged to use getQueryParams() when possible as it is PSR-7 compliant, and this method + * is not. + * + * ### PSR-7 Alternative + * + * ``` + * $value = Hash::get($request->getQueryParams(), 'Post.id', null); + * ``` + * + * @param string|null $name The name or dotted path to the query param or null to read all. + * @param mixed $default The default value if the named parameter is not set, and $name is not null. + * @return null|string|array Query data. + * @see ServerRequest::getQueryParams() + */ + public function getQuery($name = null, $default = null) + { + if ($name === null) { + return $this->query; + } + + return Hash::get($this->query, $name, $default); + } + + /** + * Provides a read/write accessor for `$this->data`. + * Allows you to use a `Hash::get()` compatible syntax for reading post data. + * + * ### Reading values. + * + * ``` + * $request->data('Post.title'); + * ``` + * + * When reading values you will get `null` for keys/values that do not exist. + * + * ### Writing values + * + * ``` + * $request->data('Post.title', 'New post!'); + * ``` + * + * You can write to any value, even paths/keys that do not exist, and the arrays + * will be created for you. + * + * @param string|null $name Dot separated name of the value to read/write + * @param mixed ...$args The data to set (deprecated) + * @return mixed|$this Either the value being read, or this so you can chain consecutive writes. + * @deprecated 3.4.0 Use withData() and getData() or getParsedBody() instead. + */ + public function data($name = null, ...$args) + { + deprecationWarning( + 'ServerRequest::data() is deprecated. ' . + 'Use withData() and getData() or getParsedBody() instead.' + ); + + if (count($args) === 1) { + $this->data = Hash::insert($this->data, $name, $args[0]); + + return $this; + } + if ($name !== null) { + return Hash::get($this->data, $name); + } + + return $this->data; + } + + /** + * Provides a safe accessor for request data. Allows + * you to use Hash::get() compatible paths. + * + * ### Reading values. + * + * ``` + * // get all data + * $request->getData(); + * + * // Read a specific field. + * $request->getData('Post.title'); + * + * // With a default value. + * $request->getData('Post.not there', 'default value'); + * ``` + * + * When reading values you will get `null` for keys/values that do not exist. + * + * @param string|null $name Dot separated name of the value to read. Or null to read all data. + * @param mixed $default The default data. + * @return null|string|array The value being read. + */ + public function getData($name = null, $default = null) + { + if ($name === null) { + return $this->data; + } + if (!is_array($this->data) && $name) { + return $default; + } + + return Hash::get($this->data, $name, $default); + } + + /** + * Safely access the values in $this->params. + * + * @param string $name The name of the parameter to get. + * @param mixed ...$args Value to set (deprecated). + * @return mixed|$this The value of the provided parameter. Will + * return false if the parameter doesn't exist or is falsey. + * @deprecated 3.4.0 Use getParam() and withParam() instead. + */ + public function param($name, ...$args) + { + deprecationWarning( + 'ServerRequest::param() is deprecated. ' . + 'Use getParam() and withParam() instead.' + ); + + if (count($args) === 1) { + $this->params = Hash::insert($this->params, $name, $args[0]); + + return $this; + } + + return $this->getParam($name); + } + + /** + * Read data from `php://input`. Useful when interacting with XML or JSON + * request body content. + * + * Getting input with a decoding function: + * + * ``` + * $this->request->input('json_decode'); + * ``` + * + * Getting input using a decoding function, and additional params: + * + * ``` + * $this->request->input('Xml::build', ['return' => 'DOMDocument']); + * ``` + * + * Any additional parameters are applied to the callback in the order they are given. + * + * @param string|null $callback A decoding callback that will convert the string data to another + * representation. Leave empty to access the raw input data. You can also + * supply additional parameters for the decoding callback using var args, see above. + * @param array ...$args The additional arguments + * @return string The decoded/processed request data. + */ + public function input($callback = null, ...$args) + { + $this->stream->rewind(); + $input = $this->stream->getContents(); + if ($callback) { + array_unshift($args, $input); + + return call_user_func_array($callback, $args); + } + + return $input; + } + + /** + * Read cookie data from the request's cookie data. + * + * @param string $key The key you want to read. + * @return null|string Either the cookie value, or null if the value doesn't exist. + * @deprecated 3.4.0 Use getCookie() instead. + */ + public function cookie($key) + { + deprecationWarning( + 'ServerRequest::cookie() is deprecated. ' . + 'Use getCookie() instead.' + ); + + if (isset($this->cookies[$key])) { + return $this->cookies[$key]; + } + + return null; + } + + /** + * Read cookie data from the request's cookie data. + * + * @param string $key The key or dotted path you want to read. + * @param string $default The default value if the cookie is not set. + * @return null|array|string Either the cookie value, or null if the value doesn't exist. + */ + public function getCookie($key, $default = null) + { + return Hash::get($this->cookies, $key, $default); + } + + /** + * Get a cookie collection based on the request's cookies + * + * The CookieCollection lets you interact with request cookies using + * `\Cake\Http\Cookie\Cookie` objects and can make converting request cookies + * into response cookies easier. + * + * This method will create a new cookie collection each time it is called. + * This is an optimization that allows fewer objects to be allocated until + * the more complex CookieCollection is needed. In general you should prefer + * `getCookie()` and `getCookieParams()` over this method. Using a CookieCollection + * is ideal if your cookies contain complex JSON encoded data. + * + * @return \Cake\Http\Cookie\CookieCollection + */ + public function getCookieCollection() + { + return CookieCollection::createFromServerRequest($this); + } + + /** + * Replace the cookies in the request with those contained in + * the provided CookieCollection. + * + * @param \Cake\Http\Cookie\CookieCollection $cookies The cookie collection + * @return static + */ + public function withCookieCollection(CookieCollection $cookies) + { + $new = clone $this; + $values = []; + foreach ($cookies as $cookie) { + $values[$cookie->getName()] = $cookie->getValue(); + } + $new->cookies = $values; + + return $new; + } + + /** + * Get all the cookie data from the request. + * + * @return array An array of cookie data. + */ + public function getCookieParams() + { + return $this->cookies; + } + + /** + * Replace the cookies and get a new request instance. + * + * @param array $cookies The new cookie data to use. + * @return static + */ + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->cookies = $cookies; + + return $new; + } + + /** + * Get the parsed request body data. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, nd the request method is POST, this will be the + * post data. For other content types, it may be the deserialized request + * body. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody() + { + return $this->data; + } + + /** + * Update the parsed body and get a new instance. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + */ + public function withParsedBody($data) + { + $new = clone $this; + $new->data = $data; + + return $new; + } + + /** + * Retrieves the HTTP protocol version as a string. + * + * @return string HTTP protocol version. + */ + public function getProtocolVersion() + { + if ($this->protocol) { + return $this->protocol; + } + + // Lazily populate this data as it is generally not used. + preg_match('/^HTTP\/([\d.]+)$/', $this->getEnv('SERVER_PROTOCOL'), $match); + $protocol = '1.1'; + if (isset($match[1])) { + $protocol = $match[1]; + } + $this->protocol = $protocol; + + return $this->protocol; + } + + /** + * Return an instance with the specified HTTP protocol version. + * + * The version string MUST contain only the HTTP version number (e.g., + * "1.1", "1.0"). + * + * @param string $version HTTP protocol version + * @return static + */ + public function withProtocolVersion($version) + { + if (!preg_match('/^(1\.[01]|2)$/', $version)) { + throw new InvalidArgumentException("Unsupported protocol version '{$version}' provided"); + } + $new = clone $this; + $new->protocol = $version; + + return $new; + } + + /** + * Get a value from the request's environment data. + * Fallback to using env() if the key is not set in the $environment property. + * + * @param string $key The key you want to read from. + * @param string|null $default Default value when trying to retrieve an environment + * variable's value that does not exist. + * @return string|null Either the environment value, or null if the value doesn't exist. + */ + public function getEnv($key, $default = null) + { + $key = strtoupper($key); + if (!array_key_exists($key, $this->_environment)) { + $this->_environment[$key] = env($key); + } + + return $this->_environment[$key] !== null ? $this->_environment[$key] : $default; + } + + /** + * Update the request with a new environment data element. + * + * Returns an updated request object. This method returns + * a *new* request object and does not mutate the request in-place. + * + * @param string $key The key you want to write to. + * @param string $value Value to set + * @return static + */ + public function withEnv($key, $value) + { + $new = clone $this; + $new->_environment[$key] = $value; + $new->clearDetectorCache(); + + return $new; + } + + /** + * Get/Set value from the request's environment data. + * Fallback to using env() if key not set in $environment property. + * + * @deprecated 3.5.0 Use getEnv()/withEnv() instead. + * @param string $key The key you want to read/write from/to. + * @param string|null $value Value to set. Default null. + * @param string|null $default Default value when trying to retrieve an environment + * variable's value that does not exist. The value parameter must be null. + * @return $this|string|null This instance if used as setter, + * if used as getter either the environment value, or null if the value doesn't exist. + */ + public function env($key, $value = null, $default = null) + { + deprecationWarning( + 'ServerRequest::env() is deprecated. ' . + 'Use getEnv()/withEnv() instead.' + ); + + if ($value !== null) { + $this->_environment[$key] = $value; + $this->clearDetectorCache(); + + return $this; + } + + $key = strtoupper($key); + if (!array_key_exists($key, $this->_environment)) { + $this->_environment[$key] = env($key); + } + + return $this->_environment[$key] !== null ? $this->_environment[$key] : $default; + } + + /** + * Allow only certain HTTP request methods, if the request method does not match + * a 405 error will be shown and the required "Allow" response header will be set. + * + * Example: + * + * $this->request->allowMethod('post'); + * or + * $this->request->allowMethod(['post', 'delete']); + * + * If the request would be GET, response header "Allow: POST, DELETE" will be set + * and a 405 error will be returned. + * + * @param string|array $methods Allowed HTTP request methods. + * @return bool true + * @throws \Cake\Http\Exception\MethodNotAllowedException + */ + public function allowMethod($methods) + { + $methods = (array)$methods; + foreach ($methods as $method) { + if ($this->is($method)) { + return true; + } + } + $allowed = strtoupper(implode(', ', $methods)); + $e = new MethodNotAllowedException(); + $e->responseHeader('Allow', $allowed); + throw $e; + } + + /** + * Read data from php://input, mocked in tests. + * + * @return string contents of php://input + */ + protected function _readInput() + { + if (empty($this->_input)) { + $fh = fopen('php://input', 'rb'); + $content = stream_get_contents($fh); + fclose($fh); + $this->_input = $content; + } + + return $this->_input; + } + + /** + * Modify data originally from `php://input`. Useful for altering json/xml data + * in middleware or DispatcherFilters before it gets to RequestHandlerComponent + * + * @param string $input A string to replace original parsed data from input() + * @return void + * @deprecated 3.4.0 This method will be removed in 4.0.0. Use withBody() instead. + */ + public function setInput($input) + { + deprecationWarning( + 'This method will be removed in 4.0.0.' . + 'Use withBody() instead.' + ); + + $stream = new Stream('php://memory', 'rw'); + $stream->write($input); + $stream->rewind(); + $this->stream = $stream; + } + + /** + * Update the request with a new request data element. + * + * Returns an updated request object. This method returns + * a *new* request object and does not mutate the request in-place. + * + * Use `withParsedBody()` if you need to replace the all request data. + * + * @param string $name The dot separated path to insert $value at. + * @param mixed $value The value to insert into the request data. + * @return static + */ + public function withData($name, $value) + { + $copy = clone $this; + $copy->data = Hash::insert($copy->data, $name, $value); + + return $copy; + } + + /** + * Update the request removing a data element. + * + * Returns an updated request object. This method returns + * a *new* request object and does not mutate the request in-place. + * + * @param string $name The dot separated path to remove. + * @return static + */ + public function withoutData($name) + { + $copy = clone $this; + $copy->data = Hash::remove($copy->data, $name); + + return $copy; + } + + /** + * Update the request with a new routing parameter + * + * Returns an updated request object. This method returns + * a *new* request object and does not mutate the request in-place. + * + * @param string $name The dot separated path to insert $value at. + * @param mixed $value The value to insert into the the request parameters. + * @return static + */ + public function withParam($name, $value) + { + $copy = clone $this; + $copy->params = Hash::insert($copy->params, $name, $value); + + return $copy; + } + + /** + * Safely access the values in $this->params. + * + * @param string $name The name or dotted path to parameter. + * @param mixed $default The default value if `$name` is not set. Default `false`. + * @return mixed + */ + public function getParam($name, $default = false) + { + return Hash::get($this->params, $name, $default); + } + + /** + * Return an instance with the specified request attribute. + * + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute($name, $value) + { + $new = clone $this; + if (in_array($name, $this->emulatedAttributes, true)) { + $new->{$name} = $value; + } else { + $new->attributes[$name] = $value; + } + + return $new; + } + + /** + * Return an instance without the specified request attribute. + * + * @param string $name The attribute name. + * @return static + * @throws \InvalidArgumentException + */ + public function withoutAttribute($name) + { + $new = clone $this; + if (in_array($name, $this->emulatedAttributes, true)) { + throw new InvalidArgumentException( + "You cannot unset '$name'. It is a required CakePHP attribute." + ); + } + unset($new->attributes[$name]); + + return $new; + } + + /** + * Read an attribute from the request, or get the default + * + * @param string $name The attribute name. + * @param mixed|null $default The default value if the attribute has not been set. + * @return mixed + */ + public function getAttribute($name, $default = null) + { + if (in_array($name, $this->emulatedAttributes, true)) { + return $this->{$name}; + } + if (array_key_exists($name, $this->attributes)) { + return $this->attributes[$name]; + } + + return $default; + } + + /** + * Get all the attributes in the request. + * + * This will include the params, webroot, base, and here attributes that CakePHP + * provides. + * + * @return array + */ + public function getAttributes() + { + $emulated = [ + 'params' => $this->params, + 'webroot' => $this->webroot, + 'base' => $this->base, + 'here' => $this->here + ]; + + return $this->attributes + $emulated; + } + + /** + * Get the uploaded file from a dotted path. + * + * @param string $path The dot separated path to the file you want. + * @return null|\Psr\Http\Message\UploadedFileInterface + */ + public function getUploadedFile($path) + { + $file = Hash::get($this->uploadedFiles, $path); + if (!$file instanceof UploadedFile) { + return null; + } + + return $file; + } + + /** + * Get the array of uploaded files from the request. + * + * @return array + */ + public function getUploadedFiles() + { + return $this->uploadedFiles; + } + + /** + * Update the request replacing the files, and creating a new instance. + * + * @param array $files An array of uploaded file objects. + * @return static + * @throws \InvalidArgumentException when $files contains an invalid object. + */ + public function withUploadedFiles(array $files) + { + $this->validateUploadedFiles($files, ''); + $new = clone $this; + $new->uploadedFiles = $files; + + return $new; + } + + /** + * Recursively validate uploaded file data. + * + * @param array $uploadedFiles The new files array to validate. + * @param string $path The path thus far. + * @return void + * @throws \InvalidArgumentException If any leaf elements are not valid files. + */ + protected function validateUploadedFiles(array $uploadedFiles, $path) + { + foreach ($uploadedFiles as $key => $file) { + if (is_array($file)) { + $this->validateUploadedFiles($file, $key . '.'); + continue; + } + + if (!$file instanceof UploadedFileInterface) { + throw new InvalidArgumentException("Invalid file at '{$path}{$key}'"); + } + } + } + + /** + * Gets the body of the message. + * + * @return \Psr\Http\Message\StreamInterface Returns the body as a stream. + */ + public function getBody() + { + return $this->stream; + } + + /** + * Return an instance with the specified message body. + * + * @param \Psr\Http\Message\StreamInterface $body The new request body + * @return static + */ + public function withBody(StreamInterface $body) + { + $new = clone $this; + $new->stream = $body; + + return $new; + } + + /** + * Retrieves the URI instance. + * + * @return \Psr\Http\Message\UriInterface Returns a UriInterface instance + * representing the URI of the request. + */ + public function getUri() + { + return $this->uri; + } + + /** + * Return an instance with the specified uri + * + * *Warning* Replacing the Uri will not update the `base`, `webroot`, + * and `url` attributes. + * + * @param \Psr\Http\Message\UriInterface $uri The new request uri + * @param bool $preserveHost Whether or not the host should be retained. + * @return static + */ + public function withUri(UriInterface $uri, $preserveHost = false) + { + $new = clone $this; + $new->uri = $uri; + + if ($preserveHost && $this->hasHeader('Host')) { + return $new; + } + + $host = $uri->getHost(); + if (!$host) { + return $new; + } + if ($uri->getPort()) { + $host .= ':' . $uri->getPort(); + } + $new->_environment['HTTP_HOST'] = $host; + + return $new; + } + + /** + * Create a new instance with a specific request-target. + * + * You can use this method to overwrite the request target that is + * inferred from the request's Uri. This also lets you change the request + * target's form to an absolute-form, authority-form or asterisk-form + * + * @link https://tools.ietf.org/html/rfc7230#section-2.7 (for the various + * request-target forms allowed in request messages) + * @param string $target The request target. + * @return static + */ + public function withRequestTarget($target) + { + $new = clone $this; + $new->requestTarget = $target; + + return $new; + } + + /** + * Retrieves the request's target. + * + * Retrieves the message's request-target either as it was requested, + * or as set with `withRequestTarget()`. By default this will return the + * application relative path without base directory, and the query string + * defined in the SERVER environment. + * + * @return string + */ + public function getRequestTarget() + { + if ($this->requestTarget !== null) { + return $this->requestTarget; + } + + $target = $this->uri->getPath(); + if ($this->uri->getQuery()) { + $target .= '?' . $this->uri->getQuery(); + } + + if (empty($target)) { + $target = '/'; + } + + return $target; + } + + /** + * Get the path of current request. + * + * @return string + * @since 3.6.1 + */ + public function getPath() + { + if ($this->requestTarget === null) { + return $this->uri->getPath(); + } + + list($path) = explode('?', $this->requestTarget); + + return $path; + } + + /** + * Array access read implementation + * + * @param string $name Name of the key being accessed. + * @return mixed + * @deprecated 3.4.0 The ArrayAccess methods will be removed in 4.0.0. Use getParam(), getData() and getQuery() instead. + */ + public function offsetGet($name) + { + deprecationWarning( + 'The ArrayAccess methods will be removed in 4.0.0.' . + 'Use getParam(), getData() and getQuery() instead.' + ); + + if (isset($this->params[$name])) { + return $this->params[$name]; + } + if ($name === 'url') { + return $this->query; + } + if ($name === 'data') { + return $this->data; + } + + return null; + } + + /** + * Array access write implementation + * + * @param string $name Name of the key being written + * @param mixed $value The value being written. + * @return void + * @deprecated 3.4.0 The ArrayAccess methods will be removed in 4.0.0. Use withParam() instead. + */ + public function offsetSet($name, $value) + { + deprecationWarning( + 'The ArrayAccess methods will be removed in 4.0.0.' . + 'Use withParam() instead.' + ); + + $this->params[$name] = $value; + } + + /** + * Array access isset() implementation + * + * @param string $name thing to check. + * @return bool + * @deprecated 3.4.0 The ArrayAccess methods will be removed in 4.0.0. Use getParam() instead. + */ + public function offsetExists($name) + { + deprecationWarning( + 'The ArrayAccess methods will be removed in 4.0.0.' . + 'Use getParam() instead.' + ); + + if ($name === 'url' || $name === 'data') { + return true; + } + + return isset($this->params[$name]); + } + + /** + * Array access unset() implementation + * + * @param string $name Name to unset. + * @return void + * @deprecated 3.4.0 The ArrayAccess methods will be removed in 4.0.0. Use withParam() instead. + */ + public function offsetUnset($name) + { + deprecationWarning( + 'The ArrayAccess methods will be removed in 4.0.0.' . + 'Use withParam() instead.' + ); + + unset($this->params[$name]); + } +} + +// @deprecated Add backwards compat alias. +class_alias('Cake\Http\ServerRequest', 'Cake\Network\Request'); diff --git a/app/vendor/cakephp/cakephp/src/Http/ServerRequestFactory.php b/app/vendor/cakephp/cakephp/src/Http/ServerRequestFactory.php new file mode 100644 index 000000000..f86b66b6c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/ServerRequestFactory.php @@ -0,0 +1,206 @@ + 'php', + 'cookiePath' => $uri->webroot + ]; + $session = Session::create($sessionConfig); + $request = new ServerRequest([ + 'environment' => $server, + 'uri' => $uri, + 'files' => $files ?: $_FILES, + 'cookies' => $cookies ?: $_COOKIE, + 'query' => $query ?: $_GET, + 'post' => $body ?: $_POST, + 'webroot' => $uri->webroot, + 'base' => $uri->base, + 'session' => $session, + ]); + + return $request; + } + + /** + * Create a new Uri instance from the provided server data. + * + * @param array $server Array of server data to build the Uri from. + * $_SERVER will be added into the $server parameter. + * @return \Psr\Http\Message\UriInterface New instance. + */ + public static function createUri(array $server = []) + { + $server += $_SERVER; + $server = static::normalizeServer($server); + $headers = static::marshalHeaders($server); + + return static::marshalUriFromServer($server, $headers); + } + + /** + * Build a UriInterface object. + * + * Add in some CakePHP specific logic/properties that help + * perserve backwards compatibility. + * + * @param array $server The server parameters. + * @param array $headers The normalized headers + * @return \Psr\Http\Message\UriInterface a constructed Uri + */ + public static function marshalUriFromServer(array $server, array $headers) + { + $uri = parent::marshalUriFromServer($server, $headers); + list($base, $webroot) = static::getBase($uri, $server); + + // Look in PATH_INFO first, as this is the exact value we need prepared + // by PHP. + $pathInfo = Hash::get($server, 'PATH_INFO'); + if ($pathInfo) { + $uri = $uri->withPath($pathInfo); + } else { + $uri = static::updatePath($base, $uri); + } + + if (!$uri->getHost()) { + $uri = $uri->withHost('localhost'); + } + + // Splat on some extra attributes to save + // some method calls. + $uri->base = $base; + $uri->webroot = $webroot; + + return $uri; + } + + /** + * Updates the request URI to remove the base directory. + * + * @param string $base The base path to remove. + * @param \Psr\Http\Message\UriInterface $uri The uri to update. + * @return \Psr\Http\Message\UriInterface The modified Uri instance. + */ + protected static function updatePath($base, $uri) + { + $path = $uri->getPath(); + if (strlen($base) > 0 && strpos($path, $base) === 0) { + $path = substr($path, strlen($base)); + } + if ($path === '/index.php' && $uri->getQuery()) { + $path = $uri->getQuery(); + } + if (empty($path) || $path === '/' || $path === '//' || $path === '/index.php') { + $path = '/'; + } + $endsWithIndex = '/webroot/index.php'; + $endsWithLength = strlen($endsWithIndex); + if (strlen($path) >= $endsWithLength && + substr($path, -$endsWithLength) === $endsWithIndex + ) { + $path = '/'; + } + + return $uri->withPath($path); + } + + /** + * Calculate the base directory and webroot directory. + * + * @param \Psr\Http\Message\UriInterface $uri The Uri instance. + * @param array $server The SERVER data to use. + * @return array An array containing the [baseDir, webroot] + */ + protected static function getBase($uri, $server) + { + $config = (array)Configure::read('App') + [ + 'base' => null, + 'webroot' => null, + 'baseUrl' => null + ]; + $base = $config['base']; + $baseUrl = $config['baseUrl']; + $webroot = $config['webroot']; + + if ($base !== false && $base !== null) { + return [$base, $base . '/']; + } + + if (!$baseUrl) { + $base = dirname(Hash::get($server, 'PHP_SELF')); + // Clean up additional / which cause following code to fail.. + $base = preg_replace('#/+#', '/', $base); + + $indexPos = strpos($base, '/' . $webroot . '/index.php'); + if ($indexPos !== false) { + $base = substr($base, 0, $indexPos) . '/' . $webroot; + } + if ($webroot === basename($base)) { + $base = dirname($base); + } + + if ($base === DIRECTORY_SEPARATOR || $base === '.') { + $base = ''; + } + $base = implode('/', array_map('rawurlencode', explode('/', $base))); + + return [$base, $base . '/']; + } + + $file = '/' . basename($baseUrl); + $base = dirname($baseUrl); + + if ($base === DIRECTORY_SEPARATOR || $base === '.') { + $base = ''; + } + $webrootDir = $base . '/'; + + $docRoot = Hash::get($server, 'DOCUMENT_ROOT'); + $docRootContainsWebroot = strpos($docRoot, $webroot); + + if (!empty($base) || !$docRootContainsWebroot) { + if (strpos($webrootDir, '/' . $webroot . '/') === false) { + $webrootDir .= $webroot . '/'; + } + } + + return [$base . $file, $webrootDir]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Session.php b/app/vendor/cakephp/cakephp/src/Http/Session.php new file mode 100644 index 000000000..165f9e6b2 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Session.php @@ -0,0 +1,619 @@ +=')) { + unset($sessionConfig['ini']['session.save_handler']); + } + + if (!isset($sessionConfig['ini']['session.cookie_httponly']) && ini_get('session.cookie_httponly') != 1) { + $sessionConfig['ini']['session.cookie_httponly'] = 1; + } + + return new static($sessionConfig); + } + + /** + * Get one of the prebaked default session configurations. + * + * @param string $name Config name. + * @return bool|array + */ + protected static function _defaultConfig($name) + { + $defaults = [ + 'php' => [ + 'cookie' => 'CAKEPHP', + 'ini' => [ + 'session.use_trans_sid' => 0, + ] + ], + 'cake' => [ + 'cookie' => 'CAKEPHP', + 'ini' => [ + 'session.use_trans_sid' => 0, + 'session.serialize_handler' => 'php', + 'session.use_cookies' => 1, + 'session.save_path' => TMP . 'sessions', + 'session.save_handler' => 'files' + ] + ], + 'cache' => [ + 'cookie' => 'CAKEPHP', + 'ini' => [ + 'session.use_trans_sid' => 0, + 'session.use_cookies' => 1, + 'session.save_handler' => 'user', + ], + 'handler' => [ + 'engine' => 'CacheSession', + 'config' => 'default' + ] + ], + 'database' => [ + 'cookie' => 'CAKEPHP', + 'ini' => [ + 'session.use_trans_sid' => 0, + 'session.use_cookies' => 1, + 'session.save_handler' => 'user', + 'session.serialize_handler' => 'php', + ], + 'handler' => [ + 'engine' => 'DatabaseSession' + ] + ] + ]; + + if (isset($defaults[$name])) { + return $defaults[$name]; + } + + return false; + } + + /** + * Constructor. + * + * ### Configuration: + * + * - timeout: The time in minutes the session should be valid for. + * - cookiePath: The url path for which session cookie is set. Maps to the + * `session.cookie_path` php.ini config. Defaults to base path of app. + * - ini: A list of php.ini directives to change before the session start. + * - handler: An array containing at least the `class` key. To be used as the session + * engine for persisting data. The rest of the keys in the array will be passed as + * the configuration array for the engine. You can set the `class` key to an already + * instantiated session handler object. + * + * @param array $config The Configuration to apply to this session object + */ + public function __construct(array $config = []) + { + if (isset($config['timeout'])) { + $config['ini']['session.gc_maxlifetime'] = 60 * $config['timeout']; + } + + if (!empty($config['cookie'])) { + $config['ini']['session.name'] = $config['cookie']; + } + + if (!isset($config['ini']['session.cookie_path'])) { + $cookiePath = empty($config['cookiePath']) ? '/' : $config['cookiePath']; + $config['ini']['session.cookie_path'] = $cookiePath; + } + + if (!empty($config['ini']) && is_array($config['ini'])) { + $this->options($config['ini']); + } + + if (!empty($config['handler']['engine'])) { + $class = $config['handler']['engine']; + unset($config['handler']['engine']); + $this->engine($class, $config['handler']); + } + + $this->_lifetime = ini_get('session.gc_maxlifetime'); + $this->_isCLI = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg'); + session_register_shutdown(); + } + + /** + * Sets the session handler instance to use for this session. + * If a string is passed for the first argument, it will be treated as the + * class name and the second argument will be passed as the first argument + * in the constructor. + * + * If an instance of a SessionHandlerInterface is provided as the first argument, + * the handler will be set to it. + * + * If no arguments are passed it will return the currently configured handler instance + * or null if none exists. + * + * @param string|\SessionHandlerInterface|null $class The session handler to use + * @param array $options the options to pass to the SessionHandler constructor + * @return \SessionHandlerInterface|null + * @throws \InvalidArgumentException + */ + public function engine($class = null, array $options = []) + { + if ($class === null) { + return $this->_engine; + } + if ($class instanceof SessionHandlerInterface) { + return $this->setEngine($class); + } + $className = App::className($class, 'Http/Session'); + + if (!$className) { + $className = App::className($class, 'Network/Session'); + if ($className) { + deprecationWarning('Session adapters should be moved to the Http/Session namespace.'); + } + } + if (!$className) { + throw new InvalidArgumentException( + sprintf('The class "%s" does not exist and cannot be used as a session engine', $class) + ); + } + + $handler = new $className($options); + if (!($handler instanceof SessionHandlerInterface)) { + throw new InvalidArgumentException( + 'The chosen SessionHandler does not implement SessionHandlerInterface, it cannot be used as an engine.' + ); + } + + return $this->setEngine($handler); + } + + /** + * Set the engine property and update the session handler in PHP. + * + * @param \SessionHandlerInterface $handler The handler to set + * @return \SessionHandlerInterface + */ + protected function setEngine(SessionHandlerInterface $handler) + { + if (!headers_sent()) { + session_set_save_handler($handler, false); + } + + return $this->_engine = $handler; + } + + /** + * Calls ini_set for each of the keys in `$options` and set them + * to the respective value in the passed array. + * + * ### Example: + * + * ``` + * $session->options(['session.use_cookies' => 1]); + * ``` + * + * @param array $options Ini options to set. + * @return void + * @throws \RuntimeException if any directive could not be set + */ + public function options(array $options) + { + if (session_status() === \PHP_SESSION_ACTIVE || headers_sent()) { + return; + } + + foreach ($options as $setting => $value) { + if (ini_set($setting, (string)$value) === false) { + throw new RuntimeException( + sprintf('Unable to configure the session, setting %s failed.', $setting) + ); + } + } + } + + /** + * Starts the Session. + * + * @return bool True if session was started + * @throws \RuntimeException if the session was already started + */ + public function start() + { + if ($this->_started) { + return true; + } + + if ($this->_isCLI) { + $_SESSION = []; + $this->id('cli'); + + return $this->_started = true; + } + + if (session_status() === \PHP_SESSION_ACTIVE) { + throw new RuntimeException('Session was already started'); + } + + if (ini_get('session.use_cookies') && headers_sent($file, $line)) { + return false; + } + + if (!session_start()) { + throw new RuntimeException('Could not start the session'); + } + + $this->_started = true; + + if ($this->_timedOut()) { + $this->destroy(); + + return $this->start(); + } + + return $this->_started; + } + + /** + * Determine if Session has already been started. + * + * @return bool True if session has been started. + */ + public function started() + { + return $this->_started || session_status() === \PHP_SESSION_ACTIVE; + } + + /** + * Returns true if given variable name is set in session. + * + * @param string|null $name Variable name to check for + * @return bool True if variable is there + */ + public function check($name = null) + { + if ($this->_hasSession() && !$this->started()) { + $this->start(); + } + + if (!isset($_SESSION)) { + return false; + } + + return Hash::get($_SESSION, $name) !== null; + } + + /** + * Returns given session variable, or all of them, if no parameters given. + * + * @param string|null $name The name of the session variable (or a path as sent to Hash.extract) + * @return string|array|null The value of the session variable, null if session not available, + * session not started, or provided name not found in the session. + */ + public function read($name = null) + { + if ($this->_hasSession() && !$this->started()) { + $this->start(); + } + + if (!isset($_SESSION)) { + return null; + } + + if ($name === null) { + return isset($_SESSION) ? $_SESSION : []; + } + + return Hash::get($_SESSION, $name); + } + + /** + * Reads and deletes a variable from session. + * + * @param string $name The key to read and remove (or a path as sent to Hash.extract). + * @return mixed The value of the session variable, null if session not available, + * session not started, or provided name not found in the session. + */ + public function consume($name) + { + if (empty($name)) { + return null; + } + $value = $this->read($name); + if ($value !== null) { + $this->_overwrite($_SESSION, Hash::remove($_SESSION, $name)); + } + + return $value; + } + + /** + * Writes value to given session variable name. + * + * @param string|array $name Name of variable + * @param mixed $value Value to write + * @return void + */ + public function write($name, $value = null) + { + if (!$this->started()) { + $this->start(); + } + + $write = $name; + if (!is_array($name)) { + $write = [$name => $value]; + } + + $data = isset($_SESSION) ? $_SESSION : []; + foreach ($write as $key => $val) { + $data = Hash::insert($data, $key, $val); + } + + $this->_overwrite($_SESSION, $data); + } + + /** + * Returns the session id. + * Calling this method will not auto start the session. You might have to manually + * assert a started session. + * + * Passing an id into it, you can also replace the session id if the session + * has not already been started. + * Note that depending on the session handler, not all characters are allowed + * within the session id. For example, the file session handler only allows + * characters in the range a-z A-Z 0-9 , (comma) and - (minus). + * + * @param string|null $id Id to replace the current session id + * @return string Session id + */ + public function id($id = null) + { + if ($id !== null && !headers_sent()) { + session_id($id); + } + + return session_id(); + } + + /** + * Removes a variable from session. + * + * @param string $name Session variable to remove + * @return void + */ + public function delete($name) + { + if ($this->check($name)) { + $this->_overwrite($_SESSION, Hash::remove($_SESSION, $name)); + } + } + + /** + * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself. + * + * @param array $old Set of old variables => values + * @param array $new New set of variable => value + * @return void + */ + protected function _overwrite(&$old, $new) + { + if (!empty($old)) { + foreach ($old as $key => $var) { + if (!isset($new[$key])) { + unset($old[$key]); + } + } + } + foreach ($new as $key => $var) { + $old[$key] = $var; + } + } + + /** + * Helper method to destroy invalid sessions. + * + * @return void + */ + public function destroy() + { + if ($this->_hasSession() && !$this->started()) { + $this->start(); + } + + if (!$this->_isCLI && session_status() === PHP_SESSION_ACTIVE) { + session_destroy(); + } + + $_SESSION = []; + $this->_started = false; + } + + /** + * Clears the session. + * + * Optionally it also clears the session id and renews the session. + * + * @param bool $renew If session should be renewed, as well. Defaults to false. + * @return void + */ + public function clear($renew = false) + { + $_SESSION = []; + if ($renew) { + $this->renew(); + } + } + + /** + * Returns whether a session exists + * + * @return bool + */ + protected function _hasSession() + { + return !ini_get('session.use_cookies') + || isset($_COOKIE[session_name()]) + || $this->_isCLI + || (ini_get('session.use_trans_sid') && isset($_GET[session_name()])); + } + + /** + * Restarts this session. + * + * @return void + */ + public function renew() + { + if (!$this->_hasSession() || $this->_isCLI) { + return; + } + + $this->start(); + $params = session_get_cookie_params(); + setcookie( + session_name(), + '', + time() - 42000, + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); + + if (session_id()) { + session_regenerate_id(true); + } + } + + /** + * Returns true if the session is no longer valid because the last time it was + * accessed was after the configured timeout. + * + * @return bool + */ + protected function _timedOut() + { + $time = $this->read('Config.time'); + $result = false; + + $checkTime = $time !== null && $this->_lifetime > 0; + if ($checkTime && (time() - $time > $this->_lifetime)) { + $result = true; + } + + $this->write('Config.time', time()); + + return $result; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Session/CacheSession.php b/app/vendor/cakephp/cakephp/src/Http/Session/CacheSession.php new file mode 100644 index 000000000..739b062ad --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Session/CacheSession.php @@ -0,0 +1,135 @@ +_options = $config; + } + + /** + * Method called on open of a database session. + * + * @param string $savePath The path where to store/retrieve the session. + * @param string $name The session name. + * @return bool Success + */ + public function open($savePath, $name) + { + return true; + } + + /** + * Method called on close of a database session. + * + * @return bool Success + */ + public function close() + { + return true; + } + + /** + * Method used to read from a cache session. + * + * @param string|int $id ID that uniquely identifies session in cache. + * @return string Session data or empty string if it does not exist. + */ + public function read($id) + { + $value = Cache::read($id, $this->_options['config']); + + if (empty($value)) { + return ''; + } + + return $value; + } + + /** + * Helper function called on write for cache sessions. + * + * @param string|int $id ID that uniquely identifies session in cache. + * @param mixed $data The data to be saved. + * @return bool True for successful write, false otherwise. + */ + public function write($id, $data) + { + if (!$id) { + return false; + } + + return (bool)Cache::write($id, $data, $this->_options['config']); + } + + /** + * Method called on the destruction of a cache session. + * + * @param string|int $id ID that uniquely identifies session in cache. + * @return bool Always true. + */ + public function destroy($id) + { + Cache::delete($id, $this->_options['config']); + + return true; + } + + /** + * Helper function called on gc for cache sessions. + * + * @param int $maxlifetime Sessions that have not updated for the last maxlifetime seconds will be removed. + * @return bool Always true. + */ + public function gc($maxlifetime) + { + Cache::gc($this->_options['config'], time() - $maxlifetime); + + return true; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Http/Session/DatabaseSession.php b/app/vendor/cakephp/cakephp/src/Http/Session/DatabaseSession.php new file mode 100644 index 000000000..914da8381 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Http/Session/DatabaseSession.php @@ -0,0 +1,185 @@ +setTableLocator($config['tableLocator']); + } + $tableLocator = $this->getTableLocator(); + + if (empty($config['model'])) { + $config = $tableLocator->exists('Sessions') ? [] : ['table' => 'sessions']; + $this->_table = $tableLocator->get('Sessions', $config); + } else { + $this->_table = $tableLocator->get($config['model']); + } + + $this->_timeout = ini_get('session.gc_maxlifetime'); + } + + /** + * Set the timeout value for sessions. + * + * Primarily used in testing. + * + * @param int $timeout The timeout duration. + * @return $this + */ + public function setTimeout($timeout) + { + $this->_timeout = $timeout; + + return $this; + } + + /** + * Method called on open of a database session. + * + * @param string $savePath The path where to store/retrieve the session. + * @param string $name The session name. + * @return bool Success + */ + public function open($savePath, $name) + { + return true; + } + + /** + * Method called on close of a database session. + * + * @return bool Success + */ + public function close() + { + return true; + } + + /** + * Method used to read from a database session. + * + * @param string|int $id ID that uniquely identifies session in database. + * @return string Session data or empty string if it does not exist. + */ + public function read($id) + { + $result = $this->_table + ->find('all') + ->select(['data']) + ->where([$this->_table->getPrimaryKey() => $id]) + ->enableHydration(false) + ->first(); + + if (empty($result)) { + return ''; + } + + if (is_string($result['data'])) { + return $result['data']; + } + + $session = stream_get_contents($result['data']); + + if ($session === false) { + return ''; + } + + return $session; + } + + /** + * Helper function called on write for database sessions. + * + * @param string|int $id ID that uniquely identifies session in database. + * @param mixed $data The data to be saved. + * @return bool True for successful write, false otherwise. + */ + public function write($id, $data) + { + if (!$id) { + return false; + } + $expires = time() + $this->_timeout; + $record = compact('data', 'expires'); + $record[$this->_table->getPrimaryKey()] = $id; + $result = $this->_table->save(new Entity($record)); + + return (bool)$result; + } + + /** + * Method called on the destruction of a database session. + * + * @param string|int $id ID that uniquely identifies session in database. + * @return bool True for successful delete, false otherwise. + */ + public function destroy($id) + { + $this->_table->delete(new Entity( + [$this->_table->getPrimaryKey() => $id], + ['markNew' => false] + )); + + return true; + } + + /** + * Helper function called on gc for database sessions. + * + * @param int $maxlifetime Sessions that have not updated for the last maxlifetime seconds will be removed. + * @return bool True on success, false on failure. + */ + public function gc($maxlifetime) + { + $this->_table->deleteAll(['expires <' => time()]); + + return true; + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/ChainMessagesLoader.php b/app/vendor/cakephp/cakephp/src/I18n/ChainMessagesLoader.php new file mode 100644 index 000000000..9b613013d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/ChainMessagesLoader.php @@ -0,0 +1,79 @@ +_loaders = $loaders; + } + + /** + * Executes this object returning the translations package as configured in + * the chain. + * + * @return \Aura\Intl\Package + * @throws \RuntimeException if any of the loaders in the chain is not a valid callable + */ + public function __invoke() + { + foreach ($this->_loaders as $k => $loader) { + if (!is_callable($loader)) { + throw new RuntimeException(sprintf( + 'Loader "%s" in the chain is not a valid callable', + $k + )); + } + + $package = $loader(); + if (!$package) { + continue; + } + + if (!($package instanceof Package)) { + throw new RuntimeException(sprintf( + 'Loader "%s" in the chain did not return a valid Package object', + $k + )); + } + + return $package; + } + + return new Package(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/Date.php b/app/vendor/cakephp/cakephp/src/I18n/Date.php new file mode 100644 index 000000000..a02172481 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/Date.php @@ -0,0 +1,135 @@ + 'day', + 'month' => 'day', + 'week' => 'day', + 'day' => 'day', + 'hour' => 'day', + 'minute' => 'day', + 'second' => 'day', + ]; + + /** + * The end of relative time telling + * + * @var string + * @see \Cake\I18n\Date::timeAgoInWords() + */ + public static $wordEnd = '+1 month'; + + /** + * Returns either a relative or a formatted absolute date depending + * on the difference between the current date and this object. + * + * ### Options: + * + * - `from` => another Date object representing the "now" date + * - `format` => a fall back format if the relative time is longer than the duration specified by end + * - `accuracy` => Specifies how accurate the date should be described (array) + * - year => The format if years > 0 (default "day") + * - month => The format if months > 0 (default "day") + * - week => The format if weeks > 0 (default "day") + * - day => The format if weeks > 0 (default "day") + * - `end` => The end of relative date telling + * - `relativeString` => The printf compatible string when outputting relative date + * - `absoluteString` => The printf compatible string when outputting absolute date + * - `timezone` => The user timezone the timestamp should be formatted in. + * + * Relative dates look something like this: + * + * - 3 weeks, 4 days ago + * - 1 day ago + * + * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using + * `i18nFormat`, see the method for the valid formatting strings. + * + * The returned string includes 'ago' or 'on' and assumes you'll properly add a word + * like 'Posted ' before the function output. + * + * NOTE: If the difference is one week or more, the lowest level of accuracy is day. + * + * @param array $options Array of options. + * @return string Relative time string. + */ + public function timeAgoInWords(array $options = []) + { + return static::diffFormatter()->dateAgoInWords($this, $options); + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/DateFormatTrait.php b/app/vendor/cakephp/cakephp/src/I18n/DateFormatTrait.php new file mode 100644 index 000000000..d2dbc42ff --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/DateFormatTrait.php @@ -0,0 +1,444 @@ +i18nFormat(static::$niceFormat, $timezone, $locale); + } + + /** + * Returns a formatted string for this time object using the preferred format and + * language for the specified locale. + * + * It is possible to specify the desired format for the string to be displayed. + * You can either pass `IntlDateFormatter` constants as the first argument of this + * function, or pass a full ICU date formatting string as specified in the following + * resource: http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details. + * + * Additional to `IntlDateFormatter` constants and date formatting string you can use + * Time::UNIX_TIMESTAMP_FORMAT to get a unix timestamp + * + * ### Examples + * + * ``` + * $time = new Time('2014-04-20 22:10'); + * $time->i18nFormat(); // outputs '4/20/14, 10:10 PM' for the en-US locale + * $time->i18nFormat(\IntlDateFormatter::FULL); // Use the full date and time format + * $time->i18nFormat([\IntlDateFormatter::FULL, \IntlDateFormatter::SHORT]); // Use full date but short time format + * $time->i18nFormat('yyyy-MM-dd HH:mm:ss'); // outputs '2014-04-20 22:10' + * $time->i18nFormat(Time::UNIX_TIMESTAMP_FORMAT); // outputs '1398031800' + * ``` + * + * If you wish to control the default format to be used for this method, you can alter + * the value of the static `Time::$defaultLocale` variable and set it to one of the + * possible formats accepted by this function. + * + * You can read about the available IntlDateFormatter constants at + * https://secure.php.net/manual/en/class.intldateformatter.php + * + * If you need to display the date in a different timezone than the one being used for + * this Time object without altering its internal state, you can pass a timezone + * string or object as the second parameter. + * + * Finally, should you need to use a different locale for displaying this time object, + * pass a locale string as the third parameter to this function. + * + * ### Examples + * + * ``` + * $time = new Time('2014-04-20 22:10'); + * $time->i18nFormat(null, null, 'de-DE'); + * $time->i18nFormat(\IntlDateFormatter::FULL, 'Europe/Berlin', 'de-DE'); + * ``` + * + * You can control the default locale to be used by setting the static variable + * `Time::$defaultLocale` to a valid locale string. If empty, the default will be + * taken from the `intl.default_locale` ini config. + * + * @param string|int|null $format Format string. + * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object + * in which the date will be displayed. The timezone stored for this object will not + * be changed. + * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR) + * @return string Formatted and translated date string + */ + public function i18nFormat($format = null, $timezone = null, $locale = null) + { + if ($format === Time::UNIX_TIMESTAMP_FORMAT) { + return $this->getTimestamp(); + } + + $time = $this; + + if ($timezone) { + // Handle the immutable and mutable object cases. + $time = clone $this; + $time = $time->timezone($timezone); + } + + $format = $format !== null ? $format : static::$_toStringFormat; + $locale = $locale ?: static::$defaultLocale; + + return $this->_formatObject($time, $format, $locale); + } + + /** + * Returns a translated and localized date string. + * Implements what IntlDateFormatter::formatObject() is in PHP 5.5+ + * + * @param \DateTime $date Date. + * @param string|int|array $format Format. + * @param string $locale The locale name in which the date should be displayed. + * @return string + */ + protected function _formatObject($date, $format, $locale) + { + $pattern = $dateFormat = $timeFormat = $calendar = null; + + if (is_array($format)) { + list($dateFormat, $timeFormat) = $format; + } elseif (is_numeric($format)) { + $dateFormat = $format; + } else { + $dateFormat = $timeFormat = IntlDateFormatter::FULL; + $pattern = $format; + } + + if (preg_match('/@calendar=(japanese|buddhist|chinese|persian|indian|islamic|hebrew|coptic|ethiopic)/', $locale)) { + $calendar = IntlDateFormatter::TRADITIONAL; + } else { + $calendar = IntlDateFormatter::GREGORIAN; + } + + $timezone = $date->getTimezone()->getName(); + $key = "{$locale}.{$dateFormat}.{$timeFormat}.{$timezone}.{$calendar}.{$pattern}"; + + if (!isset(static::$_formatters[$key])) { + if ($timezone === '+00:00' || $timezone === 'Z') { + $timezone = 'UTC'; + } elseif ($timezone[0] === '+' || $timezone[0] === '-') { + $timezone = 'GMT' . $timezone; + } + $formatter = datefmt_create( + $locale, + $dateFormat, + $timeFormat, + $timezone, + $calendar, + $pattern + ); + if (!$formatter) { + throw new RuntimeException( + 'Your version of icu does not support creating a date formatter for ' . + "`$key`. You should try to upgrade libicu and the intl extension." + ); + } + static::$_formatters[$key] = $formatter; + } + + return static::$_formatters[$key]->format($date->format('U')); + } + + /** + * {@inheritDoc} + */ + public function __toString() + { + return $this->i18nFormat(); + } + + /** + * Resets the format used to the default when converting an instance of this type to + * a string + * + * @return void + */ + public static function resetToStringFormat() + { + static::setToStringFormat([IntlDateFormatter::SHORT, IntlDateFormatter::SHORT]); + } + + /** + * Sets the default format used when type converting instances of this type to string + * + * @param string|array|int $format Format. + * @return void + */ + public static function setToStringFormat($format) + { + static::$_toStringFormat = $format; + } + + /** + * Sets the default format used when converting this object to json + * + * @param string|array|int $format Format. + * @return void + */ + public static function setJsonEncodeFormat($format) + { + static::$_jsonEncodeFormat = $format; + } + + /** + * Returns a new Time object after parsing the provided time string based on + * the passed or configured date time format. This method is locale dependent, + * Any string that is passed to this function will be interpreted as a locale + * dependent string. + * + * When no $format is provided, the `toString` format will be used. + * + * If it was impossible to parse the provided time, null will be returned. + * + * Example: + * + * ``` + * $time = Time::parseDateTime('10/13/2013 12:54am'); + * $time = Time::parseDateTime('13 Oct, 2013 13:54', 'dd MMM, y H:mm'); + * $time = Time::parseDateTime('10/10/2015', [IntlDateFormatter::SHORT, -1]); + * ``` + * + * @param string $time The time string to parse. + * @param string|array|null $format Any format accepted by IntlDateFormatter. + * @return static|null + */ + public static function parseDateTime($time, $format = null) + { + $dateFormat = $format ?: static::$_toStringFormat; + $timeFormat = $pattern = null; + + if (is_array($dateFormat)) { + list($newDateFormat, $timeFormat) = $dateFormat; + $dateFormat = $newDateFormat; + } else { + $pattern = $dateFormat; + $dateFormat = null; + } + + if (static::$_isDateInstance === null) { + static::$_isDateInstance = + is_subclass_of(static::class, ChronosDate::class) || + is_subclass_of(static::class, MutableDate::class); + } + + $defaultTimezone = static::$_isDateInstance ? 'UTC' : date_default_timezone_get(); + $formatter = datefmt_create( + static::$defaultLocale, + $dateFormat, + $timeFormat, + $defaultTimezone, + null, + $pattern + ); + $time = $formatter->parse($time); + if ($time !== false) { + $result = new static('@' . $time); + + return static::$_isDateInstance ? $result : $result->setTimezone($defaultTimezone); + } + + return null; + } + + /** + * Returns a new Time object after parsing the provided $date string based on + * the passed or configured date time format. This method is locale dependent, + * Any string that is passed to this function will be interpreted as a locale + * dependent string. + * + * When no $format is provided, the `wordFormat` format will be used. + * + * If it was impossible to parse the provided time, null will be returned. + * + * Example: + * + * ``` + * $time = Time::parseDate('10/13/2013'); + * $time = Time::parseDate('13 Oct, 2013', 'dd MMM, y'); + * $time = Time::parseDate('13 Oct, 2013', IntlDateFormatter::SHORT); + * ``` + * + * @param string $date The date string to parse. + * @param string|int|null $format Any format accepted by IntlDateFormatter. + * @return static|null + */ + public static function parseDate($date, $format = null) + { + if (is_int($format)) { + $format = [$format, -1]; + } + $format = $format ?: static::$wordFormat; + + return static::parseDateTime($date, $format); + } + + /** + * Returns a new Time object after parsing the provided $time string based on + * the passed or configured date time format. This method is locale dependent, + * Any string that is passed to this function will be interpreted as a locale + * dependent string. + * + * When no $format is provided, the IntlDateFormatter::SHORT format will be used. + * + * If it was impossible to parse the provided time, null will be returned. + * + * Example: + * + * ``` + * $time = Time::parseTime('11:23pm'); + * ``` + * + * @param string $time The time string to parse. + * @param string|int|null $format Any format accepted by IntlDateFormatter. + * @return static|null + */ + public static function parseTime($time, $format = null) + { + if (is_int($format)) { + $format = [-1, $format]; + } + $format = $format ?: [-1, IntlDateFormatter::SHORT]; + + return static::parseDateTime($time, $format); + } + + /** + * Returns a string that should be serialized when converting this object to json + * + * @return string + */ + public function jsonSerialize() + { + return $this->i18nFormat(static::$_jsonEncodeFormat); + } + + /** + * Get the difference formatter instance or overwrite the current one. + * + * @param \Cake\I18n\RelativeTimeFormatter|null $formatter The formatter instance when setting. + * @return \Cake\I18n\RelativeTimeFormatter The formatter instance. + */ + public static function diffFormatter($formatter = null) + { + if ($formatter === null) { + // Use the static property defined in chronos. + if (static::$diffFormatter === null) { + static::$diffFormatter = new RelativeTimeFormatter(); + } + + return static::$diffFormatter; + } + + return static::$diffFormatter = $formatter; + } + + /** + * Returns the data that should be displayed when debugging this object + * + * @return array + */ + public function __debugInfo() + { + return [ + 'time' => $this->toIso8601String(), + 'timezone' => $this->getTimezone()->getName(), + 'fixedNowTime' => static::hasTestNow() ? static::getTestNow()->toIso8601String() : false + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/Formatter/IcuFormatter.php b/app/vendor/cakephp/cakephp/src/I18n/Formatter/IcuFormatter.php new file mode 100644 index 000000000..c0cd00d02 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/Formatter/IcuFormatter.php @@ -0,0 +1,81 @@ +_formatMessage($locale, $message, $vars); + } + + /** + * Does the actual formatting using the MessageFormatter class + * + * @param string $locale The locale in which the message is presented. + * @param string|array $message The message to be translated + * @param array $vars The list of values to interpolate in the message + * @return string The formatted message + * @throws \Aura\Intl\Exception\CannotInstantiateFormatter if any error occurred + * while parsing the message + * @throws \Aura\Intl\Exception\CannotFormat If any error related to the passed + * variables is found + */ + protected function _formatMessage($locale, $message, $vars) + { + if ($message === '') { + return $message; + } + // Using procedural style as it showed twice as fast as + // its counterpart in PHP 5.5 + $result = MessageFormatter::formatMessage($locale, $message, $vars); + + if ($result === false) { + // The user might be interested in what went wrong, so replay the + // previous action using the object oriented style to figure out + $formatter = new MessageFormatter($locale, $message); + if (!$formatter) { + throw new CannotInstantiateFormatter(intl_get_error_message(), intl_get_error_code()); + } + + $formatter->format($vars); + throw new CannotFormat($formatter->getErrorMessage(), $formatter->getErrorCode()); + } + + return $result; + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/Formatter/SprintfFormatter.php b/app/vendor/cakephp/cakephp/src/I18n/Formatter/SprintfFormatter.php new file mode 100644 index 000000000..2cb8d5dc3 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/Formatter/SprintfFormatter.php @@ -0,0 +1,41 @@ + 'day', + 'month' => 'day', + 'week' => 'day', + 'day' => 'day', + 'hour' => 'day', + 'minute' => 'day', + 'second' => 'day', + ]; + + /** + * The end of relative time telling + * + * @var string + * @see \Cake\I18n\Date::timeAgoInWords() + */ + public static $wordEnd = '+1 month'; + + /** + * Returns either a relative or a formatted absolute date depending + * on the difference between the current date and this object. + * + * ### Options: + * + * - `from` => another Date object representing the "now" date + * - `format` => a fall back format if the relative time is longer than the duration specified by end + * - `accuracy` => Specifies how accurate the date should be described (array) + * - year => The format if years > 0 (default "day") + * - month => The format if months > 0 (default "day") + * - week => The format if weeks > 0 (default "day") + * - day => The format if weeks > 0 (default "day") + * - `end` => The end of relative date telling + * - `relativeString` => The printf compatible string when outputting relative date + * - `absoluteString` => The printf compatible string when outputting absolute date + * - `timezone` => The user timezone the timestamp should be formatted in. + * + * Relative dates look something like this: + * + * - 3 weeks, 4 days ago + * - 1 day ago + * + * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using + * `i18nFormat`, see the method for the valid formatting strings. + * + * The returned string includes 'ago' or 'on' and assumes you'll properly add a word + * like 'Posted ' before the function output. + * + * NOTE: If the difference is one week or more, the lowest level of accuracy is day. + * + * @param array $options Array of options. + * @return string Relative time string. + */ + public function timeAgoInWords(array $options = []) + { + return static::diffFormatter()->dateAgoInWords($this, $options); + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/FrozenTime.php b/app/vendor/cakephp/cakephp/src/I18n/FrozenTime.php new file mode 100644 index 000000000..500aa216e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/FrozenTime.php @@ -0,0 +1,292 @@ + 'day', + 'month' => 'day', + 'week' => 'day', + 'day' => 'hour', + 'hour' => 'minute', + 'minute' => 'minute', + 'second' => 'second', + ]; + + /** + * The end of relative time telling + * + * @var string + * @see \Cake\I18n\FrozenTime::timeAgoInWords() + */ + public static $wordEnd = '+1 month'; + + /** + * serialise the value as a Unix Timestamp + * + * @var string + */ + const UNIX_TIMESTAMP_FORMAT = 'unixTimestampFormat'; + + /** + * {@inheritDoc} + */ + public function __construct($time = null, $tz = null) + { + if ($time instanceof DateTimeInterface) { + $tz = $time->getTimezone(); + $time = $time->format('Y-m-d H:i:s'); + } + + if (is_numeric($time)) { + $time = '@' . $time; + } + + parent::__construct($time, $tz); + } + + /** + * Returns either a relative or a formatted absolute date depending + * on the difference between the current time and this object. + * + * ### Options: + * + * - `from` => another Time object representing the "now" time + * - `format` => a fall back format if the relative time is longer than the duration specified by end + * - `accuracy` => Specifies how accurate the date should be described (array) + * - year => The format if years > 0 (default "day") + * - month => The format if months > 0 (default "day") + * - week => The format if weeks > 0 (default "day") + * - day => The format if weeks > 0 (default "hour") + * - hour => The format if hours > 0 (default "minute") + * - minute => The format if minutes > 0 (default "minute") + * - second => The format if seconds > 0 (default "second") + * - `end` => The end of relative time telling + * - `relativeString` => The printf compatible string when outputting relative time + * - `absoluteString` => The printf compatible string when outputting absolute time + * - `timezone` => The user timezone the timestamp should be formatted in. + * + * Relative dates look something like this: + * + * - 3 weeks, 4 days ago + * - 15 seconds ago + * + * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using + * `i18nFormat`, see the method for the valid formatting strings + * + * The returned string includes 'ago' or 'on' and assumes you'll properly add a word + * like 'Posted ' before the function output. + * + * NOTE: If the difference is one week or more, the lowest level of accuracy is day + * + * @param array $options Array of options. + * @return string Relative time string. + */ + public function timeAgoInWords(array $options = []) + { + return static::diffFormatter()->timeAgoInWords($this, $options); + } + + /** + * Get list of timezone identifiers + * + * @param int|string|null $filter A regex to filter identifier + * Or one of DateTimeZone class constants + * @param string|null $country A two-letter ISO 3166-1 compatible country code. + * This option is only used when $filter is set to DateTimeZone::PER_COUNTRY + * @param bool|array $options If true (default value) groups the identifiers list by primary region. + * Otherwise, an array containing `group`, `abbr`, `before`, and `after` + * keys. Setting `group` and `abbr` to true will group results and append + * timezone abbreviation in the display value. Set `before` and `after` + * to customize the abbreviation wrapper. + * @return array List of timezone identifiers + * @since 2.2 + */ + public static function listTimezones($filter = null, $country = null, $options = []) + { + if (is_bool($options)) { + $options = [ + 'group' => $options, + ]; + } + $defaults = [ + 'group' => true, + 'abbr' => false, + 'before' => ' - ', + 'after' => null, + ]; + $options += $defaults; + $group = $options['group']; + + $regex = null; + if (is_string($filter)) { + $regex = $filter; + $filter = null; + } + if ($filter === null) { + $filter = DateTimeZone::ALL; + } + $identifiers = DateTimeZone::listIdentifiers($filter, $country); + + if ($regex) { + foreach ($identifiers as $key => $tz) { + if (!preg_match($regex, $tz)) { + unset($identifiers[$key]); + } + } + } + + if ($group) { + $groupedIdentifiers = []; + $now = time(); + $before = $options['before']; + $after = $options['after']; + foreach ($identifiers as $key => $tz) { + $abbr = null; + if ($options['abbr']) { + $dateTimeZone = new DateTimeZone($tz); + $trans = $dateTimeZone->getTransitions($now, $now); + $abbr = isset($trans[0]['abbr']) ? + $before . $trans[0]['abbr'] . $after : + null; + } + $item = explode('/', $tz, 2); + if (isset($item[1])) { + $groupedIdentifiers[$item[0]][$tz] = $item[1] . $abbr; + } else { + $groupedIdentifiers[$item[0]] = [$tz => $item[0] . $abbr]; + } + } + + return $groupedIdentifiers; + } + + return array_combine($identifiers, $identifiers); + } + + /** + * Returns true this instance will happen within the specified interval + * + * This overridden method provides backwards compatible behavior for integers, + * or strings with trailing spaces. This behavior is *deprecated* and will be + * removed in future versions of CakePHP. + * + * @param string|int $timeInterval the numeric value with space then time type. + * Example of valid types: 6 hours, 2 days, 1 minute. + * @return bool + */ + public function wasWithinLast($timeInterval) + { + $tmp = trim($timeInterval); + if (is_numeric($tmp)) { + deprecationWarning( + 'Passing int/numeric string into FrozenTime::wasWithinLast() is deprecated. ' . + 'Pass strings including interval eg. "6 days"' + ); + $timeInterval = $tmp . ' days'; + } + + return parent::wasWithinLast($timeInterval); + } + + /** + * Returns true this instance happened within the specified interval + * + * This overridden method provides backwards compatible behavior for integers, + * or strings with trailing spaces. This behavior is *deprecated* and will be + * removed in future versions of CakePHP. + * + * @param string|int $timeInterval the numeric value with space then time type. + * Example of valid types: 6 hours, 2 days, 1 minute. + * @return bool + */ + public function isWithinNext($timeInterval) + { + $tmp = trim($timeInterval); + if (is_numeric($tmp)) { + deprecationWarning( + 'Passing int/numeric string into FrozenTime::isWithinNext() is deprecated. ' . + 'Pass strings including interval eg. "6 days"' + ); + $timeInterval = $tmp . ' days'; + } + + return parent::isWithinNext($timeInterval); + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/I18n.php b/app/vendor/cakephp/cakephp/src/I18n/I18n.php new file mode 100644 index 000000000..8f18f529b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/I18n.php @@ -0,0 +1,426 @@ + function () { + return new SprintfFormatter(); + }, + 'default' => function () { + return new IcuFormatter(); + }, + ]), + new TranslatorFactory, + static::getLocale() + ); + + if (class_exists('Cake\Cache\Cache')) { + static::$_collection->setCacher(Cache::engine('_cake_core_')); + } + + return static::$_collection; + } + + /** + * Returns an instance of a translator that was configured for the name and passed + * locale. If no locale is passed then it takes the value returned by the `getLocale()` method. + * + * This method can be used to configure future translators, this is achieved by passing a callable + * as the last argument of this function. + * + * ### Example: + * + * ``` + * I18n::setTranslator('default', function () { + * $package = new \Aura\Intl\Package(); + * $package->setMessages([ + * 'Cake' => 'Gâteau' + * ]); + * return $package; + * }, 'fr_FR'); + * + * $translator = I18n::translator('default', 'fr_FR'); + * echo $translator->translate('Cake'); + * ``` + * + * You can also use the `Cake\I18n\MessagesFileLoader` class to load a specific + * file from a folder. For example for loading a `my_translations.po` file from + * the `src/Locale/custom` folder, you would do: + * + * ``` + * I18n::translator( + * 'default', + * 'fr_FR', + * new MessagesFileLoader('my_translations', 'custom', 'po'); + * ); + * ``` + * + * @deprecated 3.5 Use getTranslator() and setTranslator() + * @param string $name The domain of the translation messages. + * @param string|null $locale The locale for the translator. + * @param callable|null $loader A callback function or callable class responsible for + * constructing a translations package instance. + * @return \Aura\Intl\TranslatorInterface|null The configured translator. + * @throws \Aura\Intl\Exception + */ + public static function translator($name = 'default', $locale = null, callable $loader = null) + { + deprecationWarning( + 'I18n::translator() is deprecated. ' . + 'Use I18n::setTranslator()/getTranslator() instead.' + ); + if ($loader !== null) { + static::setTranslator($name, $loader, $locale); + + return null; + } + + return self::getTranslator($name, $locale); + } + + /** + * Sets a translator. + * + * Configures future translators, this is achieved by passing a callable + * as the last argument of this function. + * + * ### Example: + * + * ``` + * I18n::setTranslator('default', function () { + * $package = new \Aura\Intl\Package(); + * $package->setMessages([ + * 'Cake' => 'Gâteau' + * ]); + * return $package; + * }, 'fr_FR'); + * + * $translator = I18n::getTranslator('default', 'fr_FR'); + * echo $translator->translate('Cake'); + * ``` + * + * You can also use the `Cake\I18n\MessagesFileLoader` class to load a specific + * file from a folder. For example for loading a `my_translations.po` file from + * the `src/Locale/custom` folder, you would do: + * + * ``` + * I18n::setTranslator( + * 'default', + * new MessagesFileLoader('my_translations', 'custom', 'po'), + * 'fr_FR' + * ); + * ``` + * + * @param string $name The domain of the translation messages. + * @param callable $loader A callback function or callable class responsible for + * constructing a translations package instance. + * @param string|null $locale The locale for the translator. + * @return void + */ + public static function setTranslator($name, callable $loader, $locale = null) + { + $locale = $locale ?: static::getLocale(); + + $translators = static::translators(); + $loader = $translators->setLoaderFallback($name, $loader); + $packages = $translators->getPackages(); + $packages->set($name, $locale, $loader); + } + + /** + * Returns an instance of a translator that was configured for the name and locale. + * + * If no locale is passed then it takes the value returned by the `getLocale()` method. + * + * @param string $name The domain of the translation messages. + * @param string|null $locale The locale for the translator. + * @return \Aura\Intl\TranslatorInterface The configured translator. + * @throws \Aura\Intl\Exception + */ + public static function getTranslator($name = 'default', $locale = null) + { + $translators = static::translators(); + + if ($locale) { + $currentLocale = $translators->getLocale(); + $translators->setLocale($locale); + } + + $translator = $translators->get($name); + + if (isset($currentLocale)) { + $translators->setLocale($currentLocale); + } + + return $translator; + } + + /** + * Registers a callable object that can be used for creating new translator + * instances for the same translations domain. Loaders will be invoked whenever + * a translator object is requested for a domain that has not been configured or + * loaded already. + * + * Registering loaders is useful when you need to lazily use translations in multiple + * different locales for the same domain, and don't want to use the built-in + * translation service based of `gettext` files. + * + * Loader objects will receive two arguments: The domain name that needs to be + * built, and the locale that is requested. These objects can assemble the messages + * from any source, but must return an `Aura\Intl\Package` object. + * + * ### Example: + * + * ``` + * use Cake\I18n\MessagesFileLoader; + * I18n::config('my_domain', function ($name, $locale) { + * // Load src/Locale/$locale/filename.po + * $fileLoader = new MessagesFileLoader('filename', $locale, 'po'); + * return $fileLoader(); + * }); + * ``` + * + * You can also assemble the package object yourself: + * + * ``` + * use Aura\Intl\Package; + * I18n::config('my_domain', function ($name, $locale) { + * $package = new Package('default'); + * $messages = (...); // Fetch messages for locale from external service. + * $package->setMessages($message); + * $package->setFallback('default'); + * return $package; + * }); + * ``` + * + * @param string $name The name of the translator to create a loader for + * @param callable $loader A callable object that should return a Package + * instance to be used for assembling a new translator. + * @return void + */ + public static function config($name, callable $loader) + { + static::translators()->registerLoader($name, $loader); + } + + /** + * Sets the default locale to use for future translator instances. + * This also affects the `intl.default_locale` PHP setting. + * + * When called with no arguments it will return the currently configure + * locale as stored in the `intl.default_locale` PHP setting. + * + * @deprecated 3.5 Use setLocale() and getLocale(). + * @param string|null $locale The name of the locale to set as default. + * @return string|null The name of the default locale. + */ + public static function locale($locale = null) + { + deprecationWarning( + 'I18n::locale() is deprecated. ' . + 'Use I18n::setLocale()/getLocale() instead.' + ); + if (!empty($locale)) { + static::setLocale($locale); + + return null; + } + + return self::getLocale(); + } + + /** + * Sets the default locale to use for future translator instances. + * This also affects the `intl.default_locale` PHP setting. + * + * @param string $locale The name of the locale to set as default. + * @return void + */ + public static function setLocale($locale) + { + static::getDefaultLocale(); + Locale::setDefault($locale); + if (isset(static::$_collection)) { + static::translators()->setLocale($locale); + } + } + + /** + * Will return the currently configure locale as stored in the + * `intl.default_locale` PHP setting. + * + * @return string The name of the default locale. + */ + public static function getLocale() + { + static::getDefaultLocale(); + $current = Locale::getDefault(); + if ($current === '') { + $current = static::DEFAULT_LOCALE; + Locale::setDefault($current); + } + + return $current; + } + + /** + * This returns the default locale before any modifications, i.e. + * the value as stored in the `intl.default_locale` PHP setting before + * any manipulation by this class. + * + * @deprecated 3.5 Use getDefaultLocale() + * @return string + */ + public static function defaultLocale() + { + deprecationWarning('I18n::defaultLocale() is deprecated. Use I18n::getDefaultLocale() instead.'); + + return static::getDefaultLocale(); + } + + /** + * Returns the default locale. + * + * This returns the default locale before any modifications, i.e. + * the value as stored in the `intl.default_locale` PHP setting before + * any manipulation by this class. + * + * @return string + */ + public static function getDefaultLocale() + { + if (static::$_defaultLocale === null) { + static::$_defaultLocale = Locale::getDefault() ?: static::DEFAULT_LOCALE; + } + + return static::$_defaultLocale; + } + + /** + * Sets the name of the default messages formatter to use for future + * translator instances. + * + * By default the `default` and `sprintf` formatters are available. + * + * If called with no arguments, it will return the currently configured value. + * + * @deprecated 3.5 Use getDefaultFormatter() and setDefaultFormatter(). + * @param string|null $name The name of the formatter to use. + * @return string The name of the formatter. + */ + public static function defaultFormatter($name = null) + { + deprecationWarning( + 'I18n::defaultFormatter() is deprecated. ' . + 'Use I18n::setDefaultFormatter()/getDefaultFormatter() instead.' + ); + + return static::translators()->defaultFormatter($name); + } + + /** + * Returns the currently configured default formatter. + * + * @return string The name of the formatter. + */ + public static function getDefaultFormatter() + { + return static::translators()->defaultFormatter(); + } + + /** + * Sets the name of the default messages formatter to use for future + * translator instances. By default the `default` and `sprintf` formatters + * are available. + * + * @param string $name The name of the formatter to use. + * @return void + */ + public static function setDefaultFormatter($name) + { + static::translators()->defaultFormatter($name); + } + + /** + * Set if the domain fallback is used. + * + * @param bool $enable flag to enable or disable fallback + * @return void + */ + public static function useFallback($enable = true) + { + static::translators()->useFallback($enable); + } + + /** + * Destroys all translator instances and creates a new empty translations + * collection. + * + * @return void + */ + public static function clear() + { + static::$_collection = null; + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/LICENSE.txt b/app/vendor/cakephp/cakephp/src/I18n/LICENSE.txt new file mode 100644 index 000000000..0c4b7932c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2016, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/vendor/cakephp/cakephp/src/I18n/MessagesFileLoader.php b/app/vendor/cakephp/cakephp/src/I18n/MessagesFileLoader.php new file mode 100644 index 000000000..3fb0d60bf --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/MessagesFileLoader.php @@ -0,0 +1,183 @@ +_name = $name; + $this->_locale = $locale; + $this->_extension = $extension; + } + + /** + * Loads the translation file and parses it. Returns an instance of a translations + * package containing the messages loaded from the file. + * + * @return \Aura\Intl\Package|false + * @throws \RuntimeException if no file parser class could be found for the specified + * file extension. + */ + public function __invoke() + { + $folders = $this->translationsFolders(); + $ext = $this->_extension; + $file = false; + + $fileName = $this->_name; + $pos = strpos($fileName, '/'); + if ($pos !== false) { + $fileName = substr($fileName, $pos + 1); + } + foreach ($folders as $folder) { + $path = $folder . $fileName . ".$ext"; + if (is_file($path)) { + $file = $path; + break; + } + } + + if (!$file) { + return false; + } + + $name = ucfirst($ext); + $class = App::className($name, 'I18n\Parser', 'FileParser'); + + if (!$class) { + throw new RuntimeException(sprintf('Could not find class %s', "{$name}FileParser")); + } + + $messages = (new $class)->parse($file); + $package = new Package('default'); + $package->setMessages($messages); + + return $package; + } + + /** + * Returns the folders where the file should be looked for according to the locale + * and package name. + * + * @return array The list of folders where the translation file should be looked for + */ + public function translationsFolders() + { + $locale = Locale::parseLocale($this->_locale) + ['region' => null]; + + $folders = [ + implode('_', [$locale['language'], $locale['region']]), + $locale['language'] + ]; + + $searchPaths = []; + + $localePaths = App::path('Locale'); + if (empty($localePaths) && defined('APP')) { + $localePaths[] = APP . 'Locale' . DIRECTORY_SEPARATOR; + } + foreach ($localePaths as $path) { + foreach ($folders as $folder) { + $searchPaths[] = $path . $folder . DIRECTORY_SEPARATOR; + } + } + + // If space is not added after slash, the character after it remains lowercased + $pluginName = Inflector::camelize(str_replace('/', '/ ', $this->_name)); + if (Plugin::loaded($pluginName)) { + $basePath = Plugin::classPath($pluginName) . 'Locale' . DIRECTORY_SEPARATOR; + foreach ($folders as $folder) { + $searchPaths[] = $basePath . $folder . DIRECTORY_SEPARATOR; + } + } + + return $searchPaths; + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/Middleware/LocaleSelectorMiddleware.php b/app/vendor/cakephp/cakephp/src/I18n/Middleware/LocaleSelectorMiddleware.php new file mode 100644 index 000000000..421fe85f6 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/Middleware/LocaleSelectorMiddleware.php @@ -0,0 +1,64 @@ +locales = $locales; + } + + /** + * @param ServerRequestInterface $request The request. + * @param ResponseInterface $response The response. + * @param callable $next The next middleware to call. + * @return \Psr\Http\Message\ResponseInterface A response. + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) + { + $locale = Locale::acceptFromHttp($request->getHeaderLine('Accept-Language')); + if (!$locale) { + return $next($request, $response); + } + if (in_array($locale, $this->locales) || $this->locales === ['*']) { + I18n::setLocale($locale); + } + + return $next($request, $response); + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/Number.php b/app/vendor/cakephp/cakephp/src/I18n/Number.php new file mode 100644 index 000000000..0027bd9ed --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/Number.php @@ -0,0 +1,391 @@ + $precision, 'places' => $precision] + $options); + + return $formatter->format($value); + } + + /** + * Returns a formatted-for-humans file size. + * + * @param int $size Size in bytes + * @return string Human readable size + * @link https://book.cakephp.org/3.0/en/core-libraries/number.html#interacting-with-human-readable-values + */ + public static function toReadableSize($size) + { + switch (true) { + case $size < 1024: + return __dn('cake', '{0,number,integer} Byte', '{0,number,integer} Bytes', $size, $size); + case round($size / 1024) < 1024: + return __d('cake', '{0,number,#,###.##} KB', $size / 1024); + case round($size / 1024 / 1024, 2) < 1024: + return __d('cake', '{0,number,#,###.##} MB', $size / 1024 / 1024); + case round($size / 1024 / 1024 / 1024, 2) < 1024: + return __d('cake', '{0,number,#,###.##} GB', $size / 1024 / 1024 / 1024); + default: + return __d('cake', '{0,number,#,###.##} TB', $size / 1024 / 1024 / 1024 / 1024); + } + } + + /** + * Formats a number into a percentage string. + * + * Options: + * + * - `multiply`: Multiply the input value by 100 for decimal percentages. + * - `locale`: The locale name to use for formatting the number, e.g. fr_FR + * + * @param float $value A floating point number + * @param int $precision The precision of the returned number + * @param array $options Options + * @return string Percentage string + * @link https://book.cakephp.org/3.0/en/core-libraries/number.html#formatting-percentages + */ + public static function toPercentage($value, $precision = 2, array $options = []) + { + $options += ['multiply' => false]; + if ($options['multiply']) { + $value *= 100; + } + + return static::precision($value, $precision, $options) . '%'; + } + + /** + * Formats a number into the correct locale format + * + * Options: + * + * - `places` - Minimum number or decimals to use, e.g 0 + * - `precision` - Maximum Number of decimal places to use, e.g. 2 + * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,###.00 + * - `locale` - The locale name to use for formatting the number, e.g. fr_FR + * - `before` - The string to place before whole numbers, e.g. '[' + * - `after` - The string to place after decimal numbers, e.g. ']' + * + * @param float $value A floating point number. + * @param array $options An array with options. + * @return string Formatted number + */ + public static function format($value, array $options = []) + { + $formatter = static::formatter($options); + $options += ['before' => '', 'after' => '']; + + return $options['before'] . $formatter->format($value) . $options['after']; + } + + /** + * Parse a localized numeric string and transform it in a float point + * + * Options: + * + * - `locale` - The locale name to use for parsing the number, e.g. fr_FR + * - `type` - The formatter type to construct, set it to `currency` if you need to parse + * numbers representing money. + * + * @param string $value A numeric string. + * @param array $options An array with options. + * @return float point number + */ + public static function parseFloat($value, array $options = []) + { + $formatter = static::formatter($options); + + return (float)$formatter->parse($value, NumberFormatter::TYPE_DOUBLE); + } + + /** + * Formats a number into the correct locale format to show deltas (signed differences in value). + * + * ### Options + * + * - `places` - Minimum number or decimals to use, e.g 0 + * - `precision` - Maximum Number of decimal places to use, e.g. 2 + * - `locale` - The locale name to use for formatting the number, e.g. fr_FR + * - `before` - The string to place before whole numbers, e.g. '[' + * - `after` - The string to place after decimal numbers, e.g. ']' + * + * @param float $value A floating point number + * @param array $options Options list. + * @return string formatted delta + */ + public static function formatDelta($value, array $options = []) + { + $options += ['places' => 0]; + $value = number_format($value, $options['places'], '.', ''); + $sign = $value > 0 ? '+' : ''; + $options['before'] = isset($options['before']) ? $options['before'] . $sign : $sign; + + return static::format($value, $options); + } + + /** + * Formats a number into a currency format. + * + * ### Options + * + * - `locale` - The locale name to use for formatting the number, e.g. fr_FR + * - `fractionSymbol` - The currency symbol to use for fractional numbers. + * - `fractionPosition` - The position the fraction symbol should be placed + * valid options are 'before' & 'after'. + * - `before` - Text to display before the rendered number + * - `after` - Text to display after the rendered number + * - `zero` - The text to use for zero values, can be a string or a number. e.g. 0, 'Free!' + * - `places` - Number of decimal places to use. e.g. 2 + * - `precision` - Maximum Number of decimal places to use, e.g. 2 + * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,###.00 + * - `useIntlCode` - Whether or not to replace the currency symbol with the international + * currency code. + * + * @param float $value Value to format. + * @param string|null $currency International currency name such as 'USD', 'EUR', 'JPY', 'CAD' + * @param array $options Options list. + * @return string Number formatted as a currency. + */ + public static function currency($value, $currency = null, array $options = []) + { + $value = (float)$value; + $currency = $currency ?: static::defaultCurrency(); + + if (isset($options['zero']) && !$value) { + return $options['zero']; + } + + $formatter = static::formatter(['type' => static::FORMAT_CURRENCY] + $options); + $abs = abs($value); + if (!empty($options['fractionSymbol']) && $abs > 0 && $abs < 1) { + $value *= 100; + $pos = isset($options['fractionPosition']) ? $options['fractionPosition'] : 'after'; + + return static::format($value, ['precision' => 0, $pos => $options['fractionSymbol']]); + } + + $before = isset($options['before']) ? $options['before'] : null; + $after = isset($options['after']) ? $options['after'] : null; + + return $before . $formatter->formatCurrency($value, $currency) . $after; + } + + /** + * Getter/setter for default currency + * + * @param string|bool|null $currency Default currency string to be used by currency() + * if $currency argument is not provided. If boolean false is passed, it will clear the + * currently stored value + * @return string|null Currency + */ + public static function defaultCurrency($currency = null) + { + if (!empty($currency)) { + return self::$_defaultCurrency = $currency; + } + + if ($currency === false) { + return self::$_defaultCurrency = null; + } + + if (empty(self::$_defaultCurrency)) { + $locale = ini_get('intl.default_locale') ?: static::DEFAULT_LOCALE; + $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY); + self::$_defaultCurrency = $formatter->getTextAttribute(NumberFormatter::CURRENCY_CODE); + } + + return self::$_defaultCurrency; + } + + /** + * Returns a formatter object that can be reused for similar formatting task + * under the same locale and options. This is often a speedier alternative to + * using other methods in this class as only one formatter object needs to be + * constructed. + * + * ### Options + * + * - `locale` - The locale name to use for formatting the number, e.g. fr_FR + * - `type` - The formatter type to construct, set it to `currency` if you need to format + * numbers representing money or a NumberFormatter constant. + * - `places` - Number of decimal places to use. e.g. 2 + * - `precision` - Maximum Number of decimal places to use, e.g. 2 + * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,###.00 + * - `useIntlCode` - Whether or not to replace the currency symbol with the international + * currency code. + * + * @param array $options An array with options. + * @return \NumberFormatter The configured formatter instance + */ + public static function formatter($options = []) + { + $locale = isset($options['locale']) ? $options['locale'] : ini_get('intl.default_locale'); + + if (!$locale) { + $locale = static::DEFAULT_LOCALE; + } + + $type = NumberFormatter::DECIMAL; + if (!empty($options['type'])) { + $type = $options['type']; + if ($options['type'] === static::FORMAT_CURRENCY) { + $type = NumberFormatter::CURRENCY; + } + } + + if (!isset(static::$_formatters[$locale][$type])) { + static::$_formatters[$locale][$type] = new NumberFormatter($locale, $type); + } + + $formatter = static::$_formatters[$locale][$type]; + + $options = array_intersect_key($options, [ + 'places' => null, + 'precision' => null, + 'pattern' => null, + 'useIntlCode' => null + ]); + if (empty($options)) { + return $formatter; + } + + $formatter = clone $formatter; + + return static::_setAttributes($formatter, $options); + } + + /** + * Configure formatters. + * + * @param string $locale The locale name to use for formatting the number, e.g. fr_FR + * @param int $type The formatter type to construct. Defaults to NumberFormatter::DECIMAL. + * @param array $options See Number::formatter() for possible options. + * @return void + */ + public static function config($locale, $type = NumberFormatter::DECIMAL, array $options = []) + { + static::$_formatters[$locale][$type] = static::_setAttributes( + new NumberFormatter($locale, $type), + $options + ); + } + + /** + * Set formatter attributes + * + * @param \NumberFormatter $formatter Number formatter instance. + * @param array $options See Number::formatter() for possible options. + * @return \NumberFormatter + */ + protected static function _setAttributes(NumberFormatter $formatter, array $options = []) + { + if (isset($options['places'])) { + $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $options['places']); + } + + if (isset($options['precision'])) { + $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $options['precision']); + } + + if (!empty($options['pattern'])) { + $formatter->setPattern($options['pattern']); + } + + if (!empty($options['useIntlCode'])) { + // One of the odd things about ICU is that the currency marker in patterns + // is denoted with ¤, whereas the international code is marked with ¤¤, + // in order to use the code we need to simply duplicate the character wherever + // it appears in the pattern. + $pattern = trim(str_replace('¤', '¤¤ ', $formatter->getPattern())); + $formatter->setPattern($pattern); + } + + return $formatter; + } + + /** + * Returns a formatted integer as an ordinal number string (e.g. 1st, 2nd, 3rd, 4th, [...]) + * + * ### Options + * + * - `type` - The formatter type to construct, set it to `currency` if you need to format + * numbers representing money or a NumberFormatter constant. + * + * For all other options see formatter(). + * + * @param int|float $value An integer + * @param array $options An array with options. + * @return string + */ + public static function ordinal($value, array $options = []) + { + return static::formatter(['type' => NumberFormatter::ORDINAL] + $options)->format($value); + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/Parser/MoFileParser.php b/app/vendor/cakephp/cakephp/src/I18n/Parser/MoFileParser.php new file mode 100644 index 000000000..c8037e077 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/Parser/MoFileParser.php @@ -0,0 +1,162 @@ +_readLong($stream, $isBigEndian); + $offsetId = $this->_readLong($stream, $isBigEndian); + $offsetTranslated = $this->_readLong($stream, $isBigEndian); + + // Offset to start of translations + fread($stream, 8); + $messages = []; + + for ($i = 0; $i < $count; $i++) { + $pluralId = null; + $context = null; + $plurals = null; + + fseek($stream, $offsetId + $i * 8); + + $length = $this->_readLong($stream, $isBigEndian); + $offset = $this->_readLong($stream, $isBigEndian); + + if ($length < 1) { + continue; + } + + fseek($stream, $offset); + $singularId = fread($stream, $length); + + if (strpos($singularId, "\x04") !== false) { + list($context, $singularId) = explode("\x04", $singularId); + } + + if (strpos($singularId, "\000") !== false) { + list($singularId, $pluralId) = explode("\000", $singularId); + } + + fseek($stream, $offsetTranslated + $i * 8); + $length = $this->_readLong($stream, $isBigEndian); + $offset = $this->_readLong($stream, $isBigEndian); + fseek($stream, $offset); + $translated = fread($stream, $length); + + if ($pluralId !== null || strpos($translated, "\000") !== false) { + $translated = explode("\000", $translated); + $plurals = $pluralId !== null ? array_map('stripcslashes', $translated) : null; + $translated = $translated[0]; + } + + $singular = stripcslashes($translated); + if ($context !== null) { + $messages[$singularId]['_context'][$context] = $singular; + if ($pluralId !== null) { + $messages[$pluralId]['_context'][$context] = $plurals; + } + continue; + } + + $messages[$singularId] = $singular; + if ($pluralId !== null) { + $messages[$pluralId] = $plurals; + } + } + + fclose($stream); + + return $messages; + } + + /** + * Reads an unsigned long from stream respecting endianess. + * + * @param resource $stream The File being read. + * @param bool $isBigEndian Whether or not the current platform is Big Endian + * @return int + */ + protected function _readLong($stream, $isBigEndian) + { + $result = unpack($isBigEndian ? 'N1' : 'V1', fread($stream, 4)); + $result = current($result); + + return (int)substr($result, -8); + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/Parser/PoFileParser.php b/app/vendor/cakephp/cakephp/src/I18n/Parser/PoFileParser.php new file mode 100644 index 000000000..d0bbc4f36 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/Parser/PoFileParser.php @@ -0,0 +1,184 @@ + [], + 'translated' => null + ]; + + $messages = []; + $item = $defaults; + $stage = null; + + while ($line = fgets($stream)) { + $line = trim($line); + + if ($line === '') { + // Whitespace indicated current item is done + $this->_addMessage($messages, $item); + $item = $defaults; + $stage = null; + } elseif (substr($line, 0, 7) === 'msgid "') { + // We start a new msg so save previous + $this->_addMessage($messages, $item); + $item['ids']['singular'] = substr($line, 7, -1); + $stage = ['ids', 'singular']; + } elseif (substr($line, 0, 8) === 'msgstr "') { + $item['translated'] = substr($line, 8, -1); + $stage = ['translated']; + } elseif (substr($line, 0, 9) === 'msgctxt "') { + $item['context'] = substr($line, 9, -1); + $stage = ['context']; + } elseif ($line[0] === '"') { + switch (count($stage)) { + case 2: + $item[$stage[0]][$stage[1]] .= substr($line, 1, -1); + break; + + case 1: + $item[$stage[0]] .= substr($line, 1, -1); + break; + } + } elseif (substr($line, 0, 14) === 'msgid_plural "') { + $item['ids']['plural'] = substr($line, 14, -1); + $stage = ['ids', 'plural']; + } elseif (substr($line, 0, 7) === 'msgstr[') { + $size = strpos($line, ']'); + $row = (int)substr($line, 7, 1); + $item['translated'][$row] = substr($line, $size + 3, -1); + $stage = ['translated', $row]; + } + } + // save last item + $this->_addMessage($messages, $item); + fclose($stream); + + return $messages; + } + + /** + * Saves a translation item to the messages. + * + * @param array $messages The messages array being collected from the file + * @param array $item The current item being inspected + * @return void + */ + protected function _addMessage(array &$messages, array $item) + { + if (empty($item['ids']['singular']) && empty($item['ids']['plural'])) { + return; + } + + $singular = stripcslashes($item['ids']['singular']); + $context = isset($item['context']) ? $item['context'] : null; + $translation = $item['translated']; + + if (is_array($translation)) { + $translation = $translation[0]; + } + + $translation = stripcslashes($translation); + + if ($context !== null && !isset($messages[$singular]['_context'][$context])) { + $messages[$singular]['_context'][$context] = $translation; + } elseif (!isset($messages[$singular]['_context'][''])) { + $messages[$singular]['_context'][''] = $translation; + } + + if (isset($item['ids']['plural'])) { + $plurals = $item['translated']; + // PO are by definition indexed so sort by index. + ksort($plurals); + + // Make sure every index is filled. + end($plurals); + $count = key($plurals); + + // Fill missing spots with an empty string. + $empties = array_fill(0, $count + 1, ''); + $plurals += $empties; + ksort($plurals); + + $plurals = array_map('stripcslashes', $plurals); + $key = stripcslashes($item['ids']['plural']); + + if ($context !== null) { + $messages[Translator::PLURAL_PREFIX . $key]['_context'][$context] = $plurals; + } else { + $messages[Translator::PLURAL_PREFIX . $key]['_context'][''] = $plurals; + } + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/PluralRules.php b/app/vendor/cakephp/cakephp/src/I18n/PluralRules.php new file mode 100644 index 000000000..8a420ce7c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/PluralRules.php @@ -0,0 +1,200 @@ + plurals group used to determine + * which plural rules apply to the language + * + * @var array + */ + protected static $_rulesMap = [ + 'af' => 1, + 'am' => 2, + 'ar' => 13, + 'az' => 1, + 'be' => 3, + 'bg' => 1, + 'bh' => 2, + 'bn' => 1, + 'bo' => 0, + 'bs' => 3, + 'ca' => 1, + 'cs' => 4, + 'cy' => 14, + 'da' => 1, + 'de' => 1, + 'dz' => 0, + 'el' => 1, + 'en' => 1, + 'eo' => 1, + 'es' => 1, + 'et' => 1, + 'eu' => 1, + 'fa' => 1, + 'fi' => 1, + 'fil' => 2, + 'fo' => 1, + 'fr' => 2, + 'fur' => 1, + 'fy' => 1, + 'ga' => 5, + 'gl' => 1, + 'gu' => 1, + 'gun' => 2, + 'ha' => 1, + 'he' => 1, + 'hi' => 2, + 'hr' => 3, + 'hu' => 1, + 'id' => 0, + 'is' => 15, + 'it' => 1, + 'ja' => 0, + 'jv' => 0, + 'ka' => 0, + 'km' => 0, + 'kn' => 0, + 'ko' => 0, + 'ku' => 1, + 'lb' => 1, + 'ln' => 2, + 'lt' => 6, + 'lv' => 10, + 'mg' => 2, + 'mk' => 8, + 'ml' => 1, + 'mn' => 1, + 'mr' => 1, + 'ms' => 0, + 'mt' => 9, + 'nah' => 1, + 'nb' => 1, + 'ne' => 1, + 'nl' => 1, + 'nn' => 1, + 'no' => 1, + 'nso' => 2, + 'om' => 1, + 'or' => 1, + 'pa' => 1, + 'pap' => 1, + 'pl' => 11, + 'ps' => 1, + 'pt_pt' => 2, + 'pt' => 1, + 'ro' => 12, + 'ru' => 3, + 'sk' => 4, + 'sl' => 7, + 'so' => 1, + 'sq' => 1, + 'sr' => 3, + 'sv' => 1, + 'sw' => 1, + 'ta' => 1, + 'te' => 1, + 'th' => 0, + 'ti' => 2, + 'tk' => 1, + 'tr' => 0, + 'uk' => 3, + 'ur' => 1, + 'vi' => 0, + 'wa' => 2, + 'zh' => 0, + 'zu' => 1, + ]; + + /** + * Returns the plural form number for the passed locale corresponding + * to the countable provided in $n. + * + * @param string $locale The locale to get the rule calculated for. + * @param int|float $n The number to apply the rules to. + * @return int The plural rule number that should be used. + * @link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html + * @link https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals#List_of_Plural_Rules + */ + public static function calculate($locale, $n) + { + $locale = strtolower($locale); + + if (!isset(static::$_rulesMap[$locale])) { + $locale = explode('_', $locale)[0]; + } + + if (!isset(static::$_rulesMap[$locale])) { + return 0; + } + + switch (static::$_rulesMap[$locale]) { + case 0: + return 0; + case 1: + return $n == 1 ? 0 : 1; + case 2: + return $n > 1 ? 1 : 0; + case 3: + return $n % 10 == 1 && $n % 100 != 11 ? 0 : + (($n % 10 >= 2 && $n % 10 <= 4) && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2); + case 4: + return $n == 1 ? 0 : + ($n >= 2 && $n <= 4 ? 1 : 2); + case 5: + return $n == 1 ? 0 : + ($n == 2 ? 1 : ($n < 7 ? 2 : ($n < 11 ? 3 : 4))); + case 6: + return $n % 10 == 1 && $n % 100 != 11 ? 0 : + ($n % 10 >= 2 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2); + case 7: + return $n % 100 == 1 ? 1 : + ($n % 100 == 2 ? 2 : ($n % 100 == 3 || $n % 100 == 4 ? 3 : 0)); + case 8: + return $n % 10 == 1 ? 0 : ($n % 10 == 2 ? 1 : 2); + case 9: + return $n == 1 ? 0 : + ($n == 0 || ($n % 100 > 0 && $n % 100 <= 10) ? 1 : + ($n % 100 > 10 && $n % 100 < 20 ? 2 : 3)); + case 10: + return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n != 0 ? 1 : 2); + case 11: + return $n == 1 ? 0 : + ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2); + case 12: + return $n == 1 ? 0 : + ($n == 0 || $n % 100 > 0 && $n % 100 < 20 ? 1 : 2); + case 13: + return $n == 0 ? 0 : + ($n == 1 ? 1 : + ($n == 2 ? 2 : + ($n % 100 >= 3 && $n % 100 <= 10 ? 3 : + ($n % 100 >= 11 ? 4 : 5)))); + case 14: + return $n == 1 ? 0 : + ($n == 2 ? 1 : + ($n != 8 && $n != 11 ? 2 : 3)); + case 15: + return ($n % 10 != 1 || $n % 100 == 11) ? 1 : 0; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/README.md b/app/vendor/cakephp/cakephp/src/I18n/README.md new file mode 100644 index 000000000..4c9611631 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/README.md @@ -0,0 +1,102 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/i18n.svg?style=flat-square)](https://packagist.org/packages/cakephp/i18n) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# CakePHP Internationalization Library + +The I18n library provides a `I18n` service locator that can be used for setting +the current locale, building translation bundles and translating messages. + +Additionally, it provides the `Time` and `Number` classes which can be used to +output dates, currencies and any numbers in the right format for the specified locale. + +## Usage + +Internally, the `I18n` class uses [Aura.Intl](https://github.com/auraphp/Aura.Intl). +Getting familiar with it will help you understand how to build and manipulate translation bundles, +should you wish to create them manually instead of using the conventions this library uses. + +### Setting the Current Locale + +```php +use Cake\I18n\I18n; + +I18n::setLocale('en_US'); +``` + +### Setting path to folder containing po files. + +```php +use Cake\Core\Configure; + +Configure::write('App.paths.locales', ['/path/with/trailing/slash/']); + +Please refer to the [CakePHP Manual](https://book.cakephp.org/3.0/en/core-libraries/internationalization-and-localization.html#language-files) for details +about expected folder structure and file naming. + +### Translating a Message + +```php +echo __( + 'Hi {0,string}, your balance on the {1,date} is {2,number,currency}', + ['Charles', '2014-01-13 11:12:00', 1354.37] +); + +// Returns +Hi Charles, your balance on the Jan 13, 2014, 11:12 AM is $ 1,354.37 +``` + +### Creating Your Own Translators + +```php +use Cake\I18n\I18n; +use Aura\Intl\Package; + +I18n::translator('animals', 'fr_FR', function () { + $package = new Package( + 'default', // The formatting strategy (ICU) + 'default', // The fallback domain + ); + $package->setMessages([ + 'Dog' => 'Chien', + 'Cat' => 'Chat', + 'Bird' => 'Oiseau' + ... + ]); + + return $package; +}); + +I18n::getLocale('fr_FR'); +__d('animals', 'Dog'); // Returns "Chien" +``` + +### Formatting Time + +```php +$time = Time::now(); +echo $time; // shows '4/20/14, 10:10 PM' for the en-US locale +``` + +### Formatting Numbers + +```php +echo Number::format(100100100); +``` + +```php +echo Number::currency(123456.7890, 'EUR'); +// outputs €123,456.79 +``` + +## Documentation + +Please make sure you check the [official I18n +documentation](https://book.cakephp.org/3.0/en/core-libraries/internationalization-and-localization.html). + +The [documentation for the Time +class](https://book.cakephp.org/3.0/en/core-libraries/time.html) contains +instructions on how to configure and output time strings for selected locales. + +The [documentation for the Number +class](https://book.cakephp.org/3.0/en/core-libraries/number.html) shows how to +use the `Number` class for displaying numbers in specific locales. diff --git a/app/vendor/cakephp/cakephp/src/I18n/RelativeTimeFormatter.php b/app/vendor/cakephp/cakephp/src/I18n/RelativeTimeFormatter.php new file mode 100644 index 000000000..8d2d7d58f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/RelativeTimeFormatter.php @@ -0,0 +1,385 @@ +now($date->getTimezone()); + } + $diffInterval = $date->diff($other); + + switch (true) { + case ($diffInterval->y > 0): + $count = $diffInterval->y; + $message = __dn('cake', '{0} year', '{0} years', $count, $count); + break; + case ($diffInterval->m > 0): + $count = $diffInterval->m; + $message = __dn('cake', '{0} month', '{0} months', $count, $count); + break; + case ($diffInterval->d > 0): + $count = $diffInterval->d; + if ($count >= ChronosInterface::DAYS_PER_WEEK) { + $count = (int)($count / ChronosInterface::DAYS_PER_WEEK); + $message = __dn('cake', '{0} week', '{0} weeks', $count, $count); + } else { + $message = __dn('cake', '{0} day', '{0} days', $count, $count); + } + break; + case ($diffInterval->h > 0): + $count = $diffInterval->h; + $message = __dn('cake', '{0} hour', '{0} hours', $count, $count); + break; + case ($diffInterval->i > 0): + $count = $diffInterval->i; + $message = __dn('cake', '{0} minute', '{0} minutes', $count, $count); + break; + default: + $count = $diffInterval->s; + $message = __dn('cake', '{0} second', '{0} seconds', $count, $count); + break; + } + if ($absolute) { + return $message; + } + $isFuture = $diffInterval->invert === 1; + if ($isNow) { + return $isFuture ? __d('cake', '{0} from now', $message) : __d('cake', '{0} ago', $message); + } + + return $isFuture ? __d('cake', '{0} after', $message) : __d('cake', '{0} before', $message); + } + + /** + * Format a into a relative timestring. + * + * @param \DateTimeInterface $time The time instance to format. + * @param array $options Array of options. + * @return string Relative time string. + * @see \Cake\I18n\Time::timeAgoInWords() + */ + public function timeAgoInWords(DateTimeInterface $time, array $options = []) + { + $options = $this->_options($options, FrozenTime::class); + if ($options['timezone'] && $time instanceof ChronosInterface) { + $time = $time->timezone($options['timezone']); + } + + $now = $options['from']->format('U'); + $inSeconds = $time->format('U'); + $backwards = ($inSeconds > $now); + + $futureTime = $now; + $pastTime = $inSeconds; + if ($backwards) { + $futureTime = $inSeconds; + $pastTime = $now; + } + $diff = $futureTime - $pastTime; + + if (!$diff) { + return __d('cake', 'just now', 'just now'); + } + + if ($diff > abs($now - (new FrozenTime($options['end']))->format('U'))) { + return sprintf($options['absoluteString'], $time->i18nFormat($options['format'])); + } + + $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options); + list($fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds) = array_values($diffData); + + $relativeDate = []; + if ($fNum >= 1 && $years > 0) { + $relativeDate[] = __dn('cake', '{0} year', '{0} years', $years, $years); + } + if ($fNum >= 2 && $months > 0) { + $relativeDate[] = __dn('cake', '{0} month', '{0} months', $months, $months); + } + if ($fNum >= 3 && $weeks > 0) { + $relativeDate[] = __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); + } + if ($fNum >= 4 && $days > 0) { + $relativeDate[] = __dn('cake', '{0} day', '{0} days', $days, $days); + } + if ($fNum >= 5 && $hours > 0) { + $relativeDate[] = __dn('cake', '{0} hour', '{0} hours', $hours, $hours); + } + if ($fNum >= 6 && $minutes > 0) { + $relativeDate[] = __dn('cake', '{0} minute', '{0} minutes', $minutes, $minutes); + } + if ($fNum >= 7 && $seconds > 0) { + $relativeDate[] = __dn('cake', '{0} second', '{0} seconds', $seconds, $seconds); + } + $relativeDate = implode(', ', $relativeDate); + + // When time has passed + if (!$backwards) { + $aboutAgo = [ + 'second' => __d('cake', 'about a second ago'), + 'minute' => __d('cake', 'about a minute ago'), + 'hour' => __d('cake', 'about an hour ago'), + 'day' => __d('cake', 'about a day ago'), + 'week' => __d('cake', 'about a week ago'), + 'month' => __d('cake', 'about a month ago'), + 'year' => __d('cake', 'about a year ago') + ]; + + return $relativeDate ? sprintf($options['relativeString'], $relativeDate) : $aboutAgo[$fWord]; + } + + // When time is to come + if ($relativeDate) { + return $relativeDate; + } + $aboutIn = [ + 'second' => __d('cake', 'in about a second'), + 'minute' => __d('cake', 'in about a minute'), + 'hour' => __d('cake', 'in about an hour'), + 'day' => __d('cake', 'in about a day'), + 'week' => __d('cake', 'in about a week'), + 'month' => __d('cake', 'in about a month'), + 'year' => __d('cake', 'in about a year') + ]; + + return $aboutIn[$fWord]; + } + + /** + * Calculate the data needed to format a relative difference string. + * + * @param \DateTime $futureTime The time from the future. + * @param \DateTime $pastTime The time from the past. + * @param bool $backwards Whether or not the difference was backwards. + * @param array $options An array of options. + * @return array An array of values. + */ + protected function _diffData($futureTime, $pastTime, $backwards, $options) + { + $diff = $futureTime - $pastTime; + + // If more than a week, then take into account the length of months + if ($diff >= 604800) { + list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $futureTime)); + + list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $pastTime)); + $weeks = $days = $hours = $minutes = $seconds = 0; + + $years = $future['Y'] - $past['Y']; + $months = $future['m'] + ((12 * $years) - $past['m']); + + if ($months >= 12) { + $years = floor($months / 12); + $months -= ($years * 12); + } + if ($future['m'] < $past['m'] && $future['Y'] - $past['Y'] === 1) { + $years--; + } + + if ($future['d'] >= $past['d']) { + $days = $future['d'] - $past['d']; + } else { + $daysInPastMonth = date('t', $pastTime); + $daysInFutureMonth = date('t', mktime(0, 0, 0, $future['m'] - 1, 1, $future['Y'])); + + if (!$backwards) { + $days = ($daysInPastMonth - $past['d']) + $future['d']; + } else { + $days = ($daysInFutureMonth - $past['d']) + $future['d']; + } + + if ($future['m'] != $past['m']) { + $months--; + } + } + + if (!$months && $years >= 1 && $diff < ($years * 31536000)) { + $months = 11; + $years--; + } + + if ($months >= 12) { + $years++; + $months -= 12; + } + + if ($days >= 7) { + $weeks = floor($days / 7); + $days -= ($weeks * 7); + } + } else { + $years = $months = $weeks = 0; + $days = floor($diff / 86400); + + $diff -= ($days * 86400); + + $hours = floor($diff / 3600); + $diff -= ($hours * 3600); + + $minutes = floor($diff / 60); + $diff -= ($minutes * 60); + $seconds = $diff; + } + + $fWord = $options['accuracy']['second']; + if ($years > 0) { + $fWord = $options['accuracy']['year']; + } elseif (abs($months) > 0) { + $fWord = $options['accuracy']['month']; + } elseif (abs($weeks) > 0) { + $fWord = $options['accuracy']['week']; + } elseif (abs($days) > 0) { + $fWord = $options['accuracy']['day']; + } elseif (abs($hours) > 0) { + $fWord = $options['accuracy']['hour']; + } elseif (abs($minutes) > 0) { + $fWord = $options['accuracy']['minute']; + } + + $fNum = str_replace(['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], [1, 2, 3, 4, 5, 6, 7], $fWord); + + return [$fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds]; + } + + /** + * Format a into a relative date string. + * + * @param \DateTimeInterface $date The date to format. + * @param array $options Array of options. + * @return string Relative date string. + * @see \Cake\I18n\Date::timeAgoInWords() + */ + public function dateAgoInWords(DateTimeInterface $date, array $options = []) + { + $options = $this->_options($options, FrozenDate::class); + if ($options['timezone'] && $date instanceof ChronosInterface) { + $date = $date->timezone($options['timezone']); + } + + $now = $options['from']->format('U'); + $inSeconds = $date->format('U'); + $backwards = ($inSeconds > $now); + + $futureTime = $now; + $pastTime = $inSeconds; + if ($backwards) { + $futureTime = $inSeconds; + $pastTime = $now; + } + $diff = $futureTime - $pastTime; + + if (!$diff) { + return __d('cake', 'today'); + } + + if ($diff > abs($now - (new FrozenDate($options['end']))->format('U'))) { + return sprintf($options['absoluteString'], $date->i18nFormat($options['format'])); + } + + $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options); + list($fNum, $fWord, $years, $months, $weeks, $days) = array_values($diffData); + + $relativeDate = []; + if ($fNum >= 1 && $years > 0) { + $relativeDate[] = __dn('cake', '{0} year', '{0} years', $years, $years); + } + if ($fNum >= 2 && $months > 0) { + $relativeDate[] = __dn('cake', '{0} month', '{0} months', $months, $months); + } + if ($fNum >= 3 && $weeks > 0) { + $relativeDate[] = __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); + } + if ($fNum >= 4 && $days > 0) { + $relativeDate[] = __dn('cake', '{0} day', '{0} days', $days, $days); + } + $relativeDate = implode(', ', $relativeDate); + + // When time has passed + if (!$backwards) { + $aboutAgo = [ + 'day' => __d('cake', 'about a day ago'), + 'week' => __d('cake', 'about a week ago'), + 'month' => __d('cake', 'about a month ago'), + 'year' => __d('cake', 'about a year ago') + ]; + + return $relativeDate ? sprintf($options['relativeString'], $relativeDate) : $aboutAgo[$fWord]; + } + + // When time is to come + if ($relativeDate) { + return $relativeDate; + } + $aboutIn = [ + 'day' => __d('cake', 'in about a day'), + 'week' => __d('cake', 'in about a week'), + 'month' => __d('cake', 'in about a month'), + 'year' => __d('cake', 'in about a year') + ]; + + return $aboutIn[$fWord]; + } + + /** + * Build the options for relative date formatting. + * + * @param array $options The options provided by the user. + * @param string $class The class name to use for defaults. + * @return array Options with defaults applied. + */ + protected function _options($options, $class) + { + $options += [ + 'from' => $class::now(), + 'timezone' => null, + 'format' => $class::$wordFormat, + 'accuracy' => $class::$wordAccuracy, + 'end' => $class::$wordEnd, + 'relativeString' => __d('cake', '%s ago'), + 'absoluteString' => __d('cake', 'on %s'), + ]; + if (is_string($options['accuracy'])) { + $accuracy = $options['accuracy']; + $options['accuracy'] = []; + foreach ($class::$wordAccuracy as $key => $level) { + $options['accuracy'][$key] = $accuracy; + } + } else { + $options['accuracy'] += $class::$wordAccuracy; + } + + return $options; + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/Time.php b/app/vendor/cakephp/cakephp/src/I18n/Time.php new file mode 100644 index 000000000..0d05665df --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/Time.php @@ -0,0 +1,371 @@ + 'day', + 'month' => 'day', + 'week' => 'day', + 'day' => 'hour', + 'hour' => 'minute', + 'minute' => 'minute', + 'second' => 'second', + ]; + + /** + * The end of relative time telling + * + * @var string + * @see \Cake\I18n\Time::timeAgoInWords() + */ + public static $wordEnd = '+1 month'; + + /** + * serialise the value as a Unix Timestamp + * + * @var string + */ + const UNIX_TIMESTAMP_FORMAT = 'unixTimestampFormat'; + + /** + * {@inheritDoc} + */ + public function __construct($time = null, $tz = null) + { + if ($time instanceof DateTimeInterface) { + $tz = $time->getTimezone(); + $time = $time->format('Y-m-d H:i:s'); + } + + if (is_numeric($time)) { + $time = '@' . $time; + } + parent::__construct($time, $tz); + } + + /** + * Returns a nicely formatted date string for this object. + * + * The format to be used is stored in the static property `Time::niceFormat`. + * + * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object + * in which the date will be displayed. The timezone stored for this object will not + * be changed. + * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR) + * @return string Formatted date string + */ + public function nice($timezone = null, $locale = null) + { + return $this->i18nFormat(static::$niceFormat, $timezone, $locale); + } + + /** + * Returns true if this object represents a date within the current week + * + * @return bool + */ + public function isThisWeek() + { + return static::now($this->getTimezone())->format('W o') == $this->format('W o'); + } + + /** + * Returns true if this object represents a date within the current month + * + * @return bool + */ + public function isThisMonth() + { + return static::now($this->getTimezone())->format('m Y') == $this->format('m Y'); + } + + /** + * Returns true if this object represents a date within the current year + * + * @return bool + */ + public function isThisYear() + { + return static::now($this->getTimezone())->format('Y') == $this->format('Y'); + } + + /** + * Returns the quarter + * + * @param bool $range Range. + * @return int|array 1, 2, 3, or 4 quarter of year, or array if $range true + */ + public function toQuarter($range = false) + { + $quarter = ceil($this->format('m') / 3); + if ($range === false) { + return $quarter; + } + + $year = $this->format('Y'); + switch ($quarter) { + case 1: + return [$year . '-01-01', $year . '-03-31']; + case 2: + return [$year . '-04-01', $year . '-06-30']; + case 3: + return [$year . '-07-01', $year . '-09-30']; + case 4: + return [$year . '-10-01', $year . '-12-31']; + } + } + + /** + * Returns a UNIX timestamp. + * + * @return string UNIX timestamp + */ + public function toUnixString() + { + return $this->format('U'); + } + + /** + * Returns either a relative or a formatted absolute date depending + * on the difference between the current time and this object. + * + * ### Options: + * + * - `from` => another Time object representing the "now" time + * - `format` => a fall back format if the relative time is longer than the duration specified by end + * - `accuracy` => Specifies how accurate the date should be described (array) + * - year => The format if years > 0 (default "day") + * - month => The format if months > 0 (default "day") + * - week => The format if weeks > 0 (default "day") + * - day => The format if weeks > 0 (default "hour") + * - hour => The format if hours > 0 (default "minute") + * - minute => The format if minutes > 0 (default "minute") + * - second => The format if seconds > 0 (default "second") + * - `end` => The end of relative time telling + * - `relativeString` => The `printf` compatible string when outputting relative time + * - `absoluteString` => The `printf` compatible string when outputting absolute time + * - `timezone` => The user timezone the timestamp should be formatted in. + * + * Relative dates look something like this: + * + * - 3 weeks, 4 days ago + * - 15 seconds ago + * + * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using + * `i18nFormat`, see the method for the valid formatting strings + * + * The returned string includes 'ago' or 'on' and assumes you'll properly add a word + * like 'Posted ' before the function output. + * + * NOTE: If the difference is one week or more, the lowest level of accuracy is day + * + * @param array $options Array of options. + * @return string Relative time string. + */ + public function timeAgoInWords(array $options = []) + { + return static::diffFormatter()->timeAgoInWords($this, $options); + } + + /** + * Get list of timezone identifiers + * + * @param int|string|null $filter A regex to filter identifier + * Or one of DateTimeZone class constants + * @param string|null $country A two-letter ISO 3166-1 compatible country code. + * This option is only used when $filter is set to DateTimeZone::PER_COUNTRY + * @param bool|array $options If true (default value) groups the identifiers list by primary region. + * Otherwise, an array containing `group`, `abbr`, `before`, and `after` + * keys. Setting `group` and `abbr` to true will group results and append + * timezone abbreviation in the display value. Set `before` and `after` + * to customize the abbreviation wrapper. + * @return array List of timezone identifiers + * @since 2.2 + */ + public static function listTimezones($filter = null, $country = null, $options = []) + { + if (is_bool($options)) { + $options = [ + 'group' => $options, + ]; + } + $defaults = [ + 'group' => true, + 'abbr' => false, + 'before' => ' - ', + 'after' => null, + ]; + $options += $defaults; + $group = $options['group']; + + $regex = null; + if (is_string($filter)) { + $regex = $filter; + $filter = null; + } + if ($filter === null) { + $filter = DateTimeZone::ALL; + } + $identifiers = DateTimeZone::listIdentifiers($filter, $country); + + if ($regex) { + foreach ($identifiers as $key => $tz) { + if (!preg_match($regex, $tz)) { + unset($identifiers[$key]); + } + } + } + + if ($group) { + $groupedIdentifiers = []; + $now = time(); + $before = $options['before']; + $after = $options['after']; + foreach ($identifiers as $key => $tz) { + $abbr = null; + if ($options['abbr']) { + $dateTimeZone = new DateTimeZone($tz); + $trans = $dateTimeZone->getTransitions($now, $now); + $abbr = isset($trans[0]['abbr']) ? + $before . $trans[0]['abbr'] . $after : + null; + } + $item = explode('/', $tz, 2); + if (isset($item[1])) { + $groupedIdentifiers[$item[0]][$tz] = $item[1] . $abbr; + } else { + $groupedIdentifiers[$item[0]] = [$tz => $item[0] . $abbr]; + } + } + + return $groupedIdentifiers; + } + + return array_combine($identifiers, $identifiers); + } + + /** + * Returns true this instance will happen within the specified interval + * + * This overridden method provides backwards compatible behavior for integers, + * or strings with trailing spaces. This behavior is *deprecated* and will be + * removed in future versions of CakePHP. + * + * @param string|int $timeInterval the numeric value with space then time type. + * Example of valid types: 6 hours, 2 days, 1 minute. + * @return bool + */ + public function wasWithinLast($timeInterval) + { + $tmp = trim($timeInterval); + if (is_numeric($tmp)) { + deprecationWarning( + 'Passing int/numeric string into Time::wasWithinLast() is deprecated. ' . + 'Pass strings including interval eg. "6 days"' + ); + $timeInterval = $tmp . ' days'; + } + + return parent::wasWithinLast($timeInterval); + } + + /** + * Returns true this instance happened within the specified interval + * + * This overridden method provides backwards compatible behavior for integers, + * or strings with trailing spaces. This behavior is *deprecated* and will be + * removed in future versions of CakePHP. + * + * @param string|int $timeInterval the numeric value with space then time type. + * Example of valid types: 6 hours, 2 days, 1 minute. + * @return bool + */ + public function isWithinNext($timeInterval) + { + $tmp = trim($timeInterval); + if (is_numeric($tmp)) { + deprecationWarning( + 'Passing int/numeric string into Time::isWithinNext() is deprecated. ' . + 'Pass strings including interval eg. "6 days"' + ); + $timeInterval = $tmp . ' days'; + } + + return parent::isWithinNext($timeInterval); + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/Translator.php b/app/vendor/cakephp/cakephp/src/I18n/Translator.php new file mode 100644 index 000000000..880471a03 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/Translator.php @@ -0,0 +1,118 @@ +getMessage(static::PLURAL_PREFIX . $key); + if (!$message) { + $message = $this->getMessage($key); + } + } else { + $message = $this->getMessage($key); + if (!$message) { + $message = $this->getMessage(static::PLURAL_PREFIX . $key); + } + } + + if (!$message) { + // Fallback to the message key + $message = $key; + } + + // Check for missing/invalid context + if (isset($message['_context'])) { + $message = $this->resolveContext($key, $message, $tokensValues); + unset($tokensValues['_context']); + } + + if (!$tokensValues) { + // Fallback for plurals that were using the singular key + if (is_array($message)) { + return array_values($message + [''])[0]; + } + + return $message; + } + + // Singular message, but plural call + if (is_string($message) && isset($tokensValues['_singular'])) { + $message = [$tokensValues['_singular'], $message]; + } + + // Resolve plural form. + if (is_array($message)) { + $count = isset($tokensValues['_count']) ? $tokensValues['_count'] : 0; + $form = PluralRules::calculate($this->locale, $count); + $message = isset($message[$form]) ? $message[$form] : (string)end($message); + } + + if (strlen($message) === 0) { + $message = $key; + } + + return $this->formatter->format($this->locale, $message, $tokensValues); + } + + /** + * Resolve a message's context structure. + * + * @param string $key The message key being handled. + * @param string|array $message The message content. + * @param array $vars The variables containing the `_context` key. + * @return string + */ + protected function resolveContext($key, $message, array $vars) + { + $context = isset($vars['_context']) ? $vars['_context'] : null; + + // No or missing context, fallback to the key/first message + if ($context === null) { + if (isset($message['_context'][''])) { + return $message['_context'][''] === '' ? $key : $message['_context']['']; + } + + return current($message['_context']); + } + if (!isset($message['_context'][$context])) { + return $key; + } + if ($message['_context'][$context] === '') { + return $key; + } + + return $message['_context'][$context]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/TranslatorFactory.php b/app/vendor/cakephp/cakephp/src/I18n/TranslatorFactory.php new file mode 100644 index 000000000..79c370196 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/TranslatorFactory.php @@ -0,0 +1,63 @@ +class; + if ($fallback !== null && get_class($fallback) !== $class) { + throw new RuntimeException(sprintf( + 'Translator fallback class %s does not match Cake\I18n\Translator, try clearing your _cake_core_ cache.', + get_class($fallback) + )); + } + + return new $class($locale, $package, $formatter, $fallback); + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/TranslatorRegistry.php b/app/vendor/cakephp/cakephp/src/I18n/TranslatorRegistry.php new file mode 100644 index 000000000..df06dc61b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/TranslatorRegistry.php @@ -0,0 +1,300 @@ +registerLoader($this->_fallbackLoader, function ($name, $locale) { + $chain = new ChainMessagesLoader([ + new MessagesFileLoader($name, $locale, 'mo'), + new MessagesFileLoader($name, $locale, 'po') + ]); + + // \Aura\Intl\Package by default uses formatter configured with key "basic". + // and we want to make sure the cake domain always uses the default formatter + $formatter = $name === 'cake' ? 'default' : $this->_defaultFormatter; + $chain = function () use ($formatter, $chain) { + $package = $chain(); + $package->setFormatter($formatter); + + return $package; + }; + + return $chain; + }); + } + + /** + * Sets the CacheEngine instance used to remember translators across + * requests. + * + * @param \Cake\Cache\CacheEngine $cacher The cacher instance. + * @return void + */ + public function setCacher(CacheEngine $cacher) + { + $this->_cacher = $cacher; + } + + /** + * Gets a translator from the registry by package for a locale. + * + * @param string $name The translator package to retrieve. + * @param string|null $locale The locale to use; if empty, uses the default + * locale. + * @return \Aura\Intl\TranslatorInterface|null A translator object. + * @throws \Aura\Intl\Exception If no translator with that name could be found + * for the given locale. + */ + public function get($name, $locale = null) + { + if (!$name) { + return null; + } + + if ($locale === null) { + $locale = $this->getLocale(); + } + + if (isset($this->registry[$name][$locale])) { + return $this->registry[$name][$locale]; + } + + if (!$this->_cacher) { + return $this->registry[$name][$locale] = $this->_getTranslator($name, $locale); + } + + $key = "translations.$name.$locale"; + $translator = $this->_cacher->read($key); + if (!$translator || !$translator->getPackage()) { + $translator = $this->_getTranslator($name, $locale); + $this->_cacher->write($key, $translator); + } + + return $this->registry[$name][$locale] = $translator; + } + + /** + * Gets a translator from the registry by package for a locale. + * + * @param string $name The translator package to retrieve. + * @param string|null $locale The locale to use; if empty, uses the default + * locale. + * @return \Aura\Intl\TranslatorInterface A translator object. + */ + protected function _getTranslator($name, $locale) + { + try { + return parent::get($name, $locale); + } catch (Exception $e) { + } + + if (!isset($this->_loaders[$name])) { + $this->registerLoader($name, $this->_partialLoader()); + } + + return $this->_getFromLoader($name, $locale); + } + + /** + * Registers a loader function for a package name that will be used as a fallback + * in case no package with that name can be found. + * + * Loader callbacks will get as first argument the package name and the locale as + * the second argument. + * + * @param string $name The name of the translator package to register a loader for + * @param callable $loader A callable object that should return a Package + * @return void + */ + public function registerLoader($name, callable $loader) + { + $this->_loaders[$name] = $loader; + } + + /** + * Sets the name of the default messages formatter to use for future + * translator instances. + * + * If called with no arguments, it will return the currently configured value. + * + * @param string|null $name The name of the formatter to use. + * @return string The name of the formatter. + */ + public function defaultFormatter($name = null) + { + if ($name === null) { + return $this->_defaultFormatter; + } + + return $this->_defaultFormatter = $name; + } + + /** + * Set if the default domain fallback is used. + * + * @param bool $enable flag to enable or disable fallback + * @return void + */ + public function useFallback($enable = true) + { + $this->_useFallback = $enable; + } + + /** + * Returns a new translator instance for the given name and locale + * based of conventions. + * + * @param string $name The translation package name. + * @param string $locale The locale to create the translator for. + * @return \Aura\Intl\Translator + */ + protected function _fallbackLoader($name, $locale) + { + return $this->_loaders[$this->_fallbackLoader]($name, $locale); + } + + /** + * Returns a function that can be used as a loader for the registerLoaderMethod + * + * @return callable + */ + protected function _partialLoader() + { + return function ($name, $locale) { + return $this->_fallbackLoader($name, $locale); + }; + } + + /** + * Registers a new package by passing the register loaded function for the + * package name. + * + * @param string $name The name of the translator package + * @param string $locale The locale that should be built the package for + * @return \Aura\Intl\TranslatorInterface A translator object. + */ + protected function _getFromLoader($name, $locale) + { + $loader = $this->_loaders[$name]($name, $locale); + $package = $loader; + + if (!is_callable($loader)) { + $loader = function () use ($package) { + return $package; + }; + } + + $loader = $this->setLoaderFallback($name, $loader); + + $this->packages->set($name, $locale, $loader); + + return parent::get($name, $locale); + } + + /** + * Set domain fallback for loader. + * + * @param string $name The name of the loader domain + * @param callable $loader invokable loader + * @return callable loader + */ + public function setLoaderFallback($name, callable $loader) + { + $fallbackDomain = 'default'; + if (!$this->_useFallback || $name === $fallbackDomain) { + return $loader; + } + $loader = function () use ($loader, $fallbackDomain) { + /* @var \Aura\Intl\Package $package */ + $package = $loader(); + if (!$package->getFallback()) { + $package->setFallback($fallbackDomain); + } + + return $package; + }; + + return $loader; + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/composer.json b/app/vendor/cakephp/cakephp/src/I18n/composer.json new file mode 100644 index 000000000..7ffc86fa6 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/composer.json @@ -0,0 +1,48 @@ +{ + "name": "cakephp/i18n", + "description": "CakePHP Internationalization library with support for messages translation and dates and numbers localization", + "type": "library", + "keywords": [ + "cakephp", + "i18n", + "internationalisation", + "internationalization", + "localisation", + "localization", + "translation", + "date", + "number" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/i18n/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/i18n" + }, + "require": { + "php": ">=5.6.0", + "ext-intl": "*", + "cakephp/core": "^3.6.0", + "cakephp/chronos": "^1.0.0", + "aura/intl": "^3.0.0" + }, + "suggest": { + "cakephp/cache": "Require this if you want automatic caching of translators" + }, + "autoload": { + "psr-4": { + "Cake\\I18n\\": "." + }, + "files": [ + "functions.php" + ] + } +} diff --git a/app/vendor/cakephp/cakephp/src/I18n/functions.php b/app/vendor/cakephp/cakephp/src/I18n/functions.php new file mode 100644 index 000000000..6d015b454 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/I18n/functions.php @@ -0,0 +1,251 @@ +translate($singular, $args); + } + +} + +if (!function_exists('__n')) { + /** + * Returns correct plural form of message identified by $singular and $plural for count $count. + * Some languages have more than one form for plural messages dependent on the count. + * + * @param string $singular Singular text to translate. + * @param string $plural Plural text. + * @param int $count Count. + * @param array ...$args Array with arguments or multiple arguments in function. + * @return string|null Plural form of translated string, or null if invalid. + * @throws \Aura\Intl\Exception + * @link https://book.cakephp.org/3.0/en/core-libraries/global-constants-and-functions.html#__n + */ + function __n($singular, $plural, $count, ...$args) + { + if (!$singular) { + return null; + } + if (isset($args[0]) && is_array($args[0])) { + $args = $args[0]; + } + + return I18n::getTranslator()->translate( + $plural, + ['_count' => $count, '_singular' => $singular] + $args + ); + } + +} + +if (!function_exists('__d')) { + /** + * Allows you to override the current domain for a single message lookup. + * + * @param string $domain Domain. + * @param string $msg String to translate. + * @param array ...$args Array with arguments or multiple arguments in function. + * @return string|null Translated string. + * @throws \Aura\Intl\Exception + * @link https://book.cakephp.org/3.0/en/core-libraries/global-constants-and-functions.html#__d + */ + function __d($domain, $msg, ...$args) + { + if (!$msg) { + return null; + } + if (isset($args[0]) && is_array($args[0])) { + $args = $args[0]; + } + + return I18n::getTranslator($domain)->translate($msg, $args); + } + +} + +if (!function_exists('__dn')) { + /** + * Allows you to override the current domain for a single plural message lookup. + * Returns correct plural form of message identified by $singular and $plural for count $count + * from domain $domain. + * + * @param string $domain Domain. + * @param string $singular Singular string to translate. + * @param string $plural Plural. + * @param int $count Count. + * @param array ...$args Array with arguments or multiple arguments in function. + * @return string|null Plural form of translated string. + * @throws \Aura\Intl\Exception + * @link https://book.cakephp.org/3.0/en/core-libraries/global-constants-and-functions.html#__dn + */ + function __dn($domain, $singular, $plural, $count, ...$args) + { + if (!$singular) { + return null; + } + if (isset($args[0]) && is_array($args[0])) { + $args = $args[0]; + } + + return I18n::getTranslator($domain)->translate( + $plural, + ['_count' => $count, '_singular' => $singular] + $args + ); + } + +} + +if (!function_exists('__x')) { + /** + * Returns a translated string if one is found; Otherwise, the submitted message. + * The context is a unique identifier for the translations string that makes it unique + * within the same domain. + * + * @param string $context Context of the text. + * @param string $singular Text to translate. + * @param array ...$args Array with arguments or multiple arguments in function. + * @return string|null Translated string. + * @throws \Aura\Intl\Exception + * @link https://book.cakephp.org/3.0/en/core-libraries/global-constants-and-functions.html#__x + */ + function __x($context, $singular, ...$args) + { + if (!$singular) { + return null; + } + if (isset($args[0]) && is_array($args[0])) { + $args = $args[0]; + } + + return I18n::getTranslator()->translate($singular, ['_context' => $context] + $args); + } + +} + +if (!function_exists('__xn')) { + /** + * Returns correct plural form of message identified by $singular and $plural for count $count. + * Some languages have more than one form for plural messages dependent on the count. + * The context is a unique identifier for the translations string that makes it unique + * within the same domain. + * + * @param string $context Context of the text. + * @param string $singular Singular text to translate. + * @param string $plural Plural text. + * @param int $count Count. + * @param array ...$args Array with arguments or multiple arguments in function. + * @return string|null Plural form of translated string. + * @throws \Aura\Intl\Exception + * @link https://book.cakephp.org/3.0/en/core-libraries/global-constants-and-functions.html#__xn + */ + function __xn($context, $singular, $plural, $count, ...$args) + { + if (!$singular) { + return null; + } + if (isset($args[0]) && is_array($args[0])) { + $args = $args[0]; + } + + return I18n::getTranslator()->translate( + $plural, + ['_count' => $count, '_singular' => $singular, '_context' => $context] + $args + ); + } + +} + +if (!function_exists('__dx')) { + /** + * Allows you to override the current domain for a single message lookup. + * The context is a unique identifier for the translations string that makes it unique + * within the same domain. + * + * @param string $domain Domain. + * @param string $context Context of the text. + * @param string $msg String to translate. + * @param array ...$args Array with arguments or multiple arguments in function. + * @return string|null Translated string. + * @throws \Aura\Intl\Exception + * @link https://book.cakephp.org/3.0/en/core-libraries/global-constants-and-functions.html#__dx + */ + function __dx($domain, $context, $msg, ...$args) + { + if (!$msg) { + return null; + } + if (isset($args[0]) && is_array($args[0])) { + $args = $args[0]; + } + + return I18n::getTranslator($domain)->translate( + $msg, + ['_context' => $context] + $args + ); + } + +} + +if (!function_exists('__dxn')) { + /** + * Returns correct plural form of message identified by $singular and $plural for count $count. + * Allows you to override the current domain for a single message lookup. + * The context is a unique identifier for the translations string that makes it unique + * within the same domain. + * + * @param string $domain Domain. + * @param string $context Context of the text. + * @param string $singular Singular text to translate. + * @param string $plural Plural text. + * @param int $count Count. + * @param array ...$args Array with arguments or multiple arguments in function. + * @return string|null Plural form of translated string. + * @throws \Aura\Intl\Exception + * @link https://book.cakephp.org/3.0/en/core-libraries/global-constants-and-functions.html#__dxn + */ + function __dxn($domain, $context, $singular, $plural, $count, ...$args) + { + if (!$singular) { + return null; + } + if (isset($args[0]) && is_array($args[0])) { + $args = $args[0]; + } + + return I18n::getTranslator($domain)->translate( + $plural, + ['_count' => $count, '_singular' => $singular, '_context' => $context] + $args + ); + } + +} diff --git a/app/vendor/cakephp/cakephp/src/Log/Engine/BaseLog.php b/app/vendor/cakephp/cakephp/src/Log/Engine/BaseLog.php new file mode 100644 index 000000000..c35e53363 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Log/Engine/BaseLog.php @@ -0,0 +1,115 @@ + [], + 'scopes' => [] + ]; + + /** + * __construct method + * + * @param array $config Configuration array + */ + public function __construct(array $config = []) + { + $this->setConfig($config); + + if (!is_array($this->_config['scopes']) && $this->_config['scopes'] !== false) { + $this->_config['scopes'] = (array)$this->_config['scopes']; + } + + if (!is_array($this->_config['levels'])) { + $this->_config['levels'] = (array)$this->_config['levels']; + } + + if (!empty($this->_config['types']) && empty($this->_config['levels'])) { + $this->_config['levels'] = (array)$this->_config['types']; + } + } + + /** + * Get the levels this logger is interested in. + * + * @return array + */ + public function levels() + { + return $this->_config['levels']; + } + + /** + * Get the scopes this logger is interested in. + * + * @return array + */ + public function scopes() + { + return $this->_config['scopes']; + } + + /** + * Converts to string the provided data so it can be logged. The context + * can optionally be used by log engines to interpolate variables + * or add additional info to the logged message. + * + * @param mixed $data The data to be converted to string and logged. + * @param array $context Additional logging information for the message. + * @return string + */ + protected function _format($data, array $context = []) + { + if (is_string($data)) { + return $data; + } + + $isObject = is_object($data); + + if ($isObject && $data instanceof EntityInterface) { + return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + + if ($isObject && method_exists($data, '__toString')) { + return (string)$data; + } + + if ($isObject && $data instanceof JsonSerializable) { + return json_encode($data, JSON_UNESCAPED_UNICODE); + } + + return print_r($data, true); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Log/Engine/ConsoleLog.php b/app/vendor/cakephp/cakephp/src/Log/Engine/ConsoleLog.php new file mode 100644 index 000000000..db8e72ba0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Log/Engine/ConsoleLog.php @@ -0,0 +1,96 @@ + 'php://stderr', + 'levels' => null, + 'scopes' => [], + 'outputAs' => 'see constructor' + ]; + + /** + * Output stream + * + * @var \Cake\Console\ConsoleOutput + */ + protected $_output; + + /** + * Constructs a new Console Logger. + * + * Config + * + * - `levels` string or array, levels the engine is interested in + * - `scopes` string or array, scopes the engine is interested in + * - `stream` the path to save logs on. + * - `outputAs` integer or ConsoleOutput::[RAW|PLAIN|COLOR] + * + * @param array $config Options for the FileLog, see above. + * @throws \InvalidArgumentException + */ + public function __construct(array $config = []) + { + if ((DIRECTORY_SEPARATOR === '\\' && !(bool)env('ANSICON') && env('ConEmuANSI') !== 'ON') || + (function_exists('posix_isatty') && !posix_isatty($this->_output)) + ) { + $this->_defaultConfig['outputAs'] = ConsoleOutput::PLAIN; + } else { + $this->_defaultConfig['outputAs'] = ConsoleOutput::COLOR; + } + + parent::__construct($config); + + $config = $this->_config; + if ($config['stream'] instanceof ConsoleOutput) { + $this->_output = $config['stream']; + } elseif (is_string($config['stream'])) { + $this->_output = new ConsoleOutput($config['stream']); + } else { + throw new InvalidArgumentException('`stream` not a ConsoleOutput nor string'); + } + $this->_output->setOutputAs($config['outputAs']); + } + + /** + * Implements writing to console. + * + * @param string $level The severity level of log you are making. + * @param string $message The message you want to log. + * @param array $context Additional information about the logged message + * @return bool success of write. + */ + public function log($level, $message, array $context = []) + { + $message = $this->_format($message, $context); + $output = date('Y-m-d H:i:s') . ' ' . ucfirst($level) . ': ' . $message; + + return (bool)$this->_output->write(sprintf('<%s>%s', $level, $output, $level)); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Log/Engine/FileLog.php b/app/vendor/cakephp/cakephp/src/Log/Engine/FileLog.php new file mode 100644 index 000000000..5e424be34 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Log/Engine/FileLog.php @@ -0,0 +1,212 @@ + null, + 'file' => null, + 'types' => null, + 'levels' => [], + 'scopes' => [], + 'rotate' => 10, + 'size' => 10485760, // 10MB + 'mask' => null, + ]; + + /** + * Path to save log files on. + * + * @var string|null + */ + protected $_path; + + /** + * The name of the file to save logs into. + * + * @var string|null + */ + protected $_file; + + /** + * Max file size, used for log file rotation. + * + * @var int|null + */ + protected $_size; + + /** + * Sets protected properties based on config provided + * + * @param array $config Configuration array + */ + public function __construct(array $config = []) + { + parent::__construct($config); + + if (!empty($this->_config['path'])) { + $this->_path = $this->_config['path']; + } + if ($this->_path !== null && + Configure::read('debug') && + !is_dir($this->_path) + ) { + mkdir($this->_path, 0775, true); + } + + if (!empty($this->_config['file'])) { + $this->_file = $this->_config['file']; + if (substr($this->_file, -4) !== '.log') { + $this->_file .= '.log'; + } + } + + if (!empty($this->_config['size'])) { + if (is_numeric($this->_config['size'])) { + $this->_size = (int)$this->_config['size']; + } else { + $this->_size = Text::parseFileSize($this->_config['size']); + } + } + } + + /** + * Implements writing to log files. + * + * @param string $level The severity level of the message being written. + * See Cake\Log\Log::$_levels for list of possible levels. + * @param string $message The message you want to log. + * @param array $context Additional information about the logged message + * @return bool success of write. + */ + public function log($level, $message, array $context = []) + { + $message = $this->_format($message, $context); + $output = date('Y-m-d H:i:s') . ' ' . ucfirst($level) . ': ' . $message . "\n"; + $filename = $this->_getFilename($level); + if ($this->_size) { + $this->_rotateFile($filename); + } + + $pathname = $this->_path . $filename; + $mask = $this->_config['mask']; + if (!$mask) { + return file_put_contents($pathname, $output, FILE_APPEND); + } + + $exists = file_exists($pathname); + $result = file_put_contents($pathname, $output, FILE_APPEND); + static $selfError = false; + + if (!$selfError && !$exists && !chmod($pathname, (int)$mask)) { + $selfError = true; + trigger_error(vsprintf( + 'Could not apply permission mask "%s" on log file "%s"', + [$mask, $pathname] + ), E_USER_WARNING); + $selfError = false; + } + + return $result; + } + + /** + * Get filename + * + * @param string $level The level of log. + * @return string File name + */ + protected function _getFilename($level) + { + $debugTypes = ['notice', 'info', 'debug']; + + if ($this->_file) { + $filename = $this->_file; + } elseif ($level === 'error' || $level === 'warning') { + $filename = 'error.log'; + } elseif (in_array($level, $debugTypes)) { + $filename = 'debug.log'; + } else { + $filename = $level . '.log'; + } + + return $filename; + } + + /** + * Rotate log file if size specified in config is reached. + * Also if `rotate` count is reached oldest file is removed. + * + * @param string $filename Log file name + * @return bool|null True if rotated successfully or false in case of error. + * Null if file doesn't need to be rotated. + */ + protected function _rotateFile($filename) + { + $filePath = $this->_path . $filename; + clearstatcache(true, $filePath); + + if (!file_exists($filePath) || + filesize($filePath) < $this->_size + ) { + return null; + } + + $rotate = $this->_config['rotate']; + if ($rotate === 0) { + $result = unlink($filePath); + } else { + $result = rename($filePath, $filePath . '.' . time()); + } + + $files = glob($filePath . '.*'); + if ($files) { + $filesToDelete = count($files) - $rotate; + while ($filesToDelete > 0) { + unlink(array_shift($files)); + $filesToDelete--; + } + } + + return $result; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Log/Engine/SyslogLog.php b/app/vendor/cakephp/cakephp/src/Log/Engine/SyslogLog.php new file mode 100644 index 000000000..503c9ce2a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Log/Engine/SyslogLog.php @@ -0,0 +1,150 @@ + 'Syslog', + * 'levels' => ['emergency', 'alert', 'critical', 'error'], + * 'format' => "%s: My-App - %s", + * 'prefix' => 'Web Server 01' + * ]); + * ``` + * + * @var array + */ + protected $_defaultConfig = [ + 'levels' => [], + 'scopes' => [], + 'format' => '%s: %s', + 'flag' => LOG_ODELAY, + 'prefix' => '', + 'facility' => LOG_USER + ]; + + /** + * Used to map the string names back to their LOG_* constants + * + * @var int[] + */ + protected $_levelMap = [ + 'emergency' => LOG_EMERG, + 'alert' => LOG_ALERT, + 'critical' => LOG_CRIT, + 'error' => LOG_ERR, + 'warning' => LOG_WARNING, + 'notice' => LOG_NOTICE, + 'info' => LOG_INFO, + 'debug' => LOG_DEBUG + ]; + + /** + * Whether the logger connection is open or not + * + * @var bool + */ + protected $_open = false; + + /** + * Writes a message to syslog + * + * Map the $level back to a LOG_ constant value, split multi-line messages into multiple + * log messages, pass all messages through the format defined in the configuration + * + * @param string $level The severity level of log you are making. + * @param string $message The message you want to log. + * @param array $context Additional information about the logged message + * @return bool success of write. + */ + public function log($level, $message, array $context = []) + { + if (!$this->_open) { + $config = $this->_config; + $this->_open($config['prefix'], $config['flag'], $config['facility']); + $this->_open = true; + } + + $priority = LOG_DEBUG; + if (isset($this->_levelMap[$level])) { + $priority = $this->_levelMap[$level]; + } + + $messages = explode("\n", $this->_format($message, $context)); + foreach ($messages as $message) { + $message = sprintf($this->_config['format'], $level, $message); + $this->_write($priority, $message); + } + + return true; + } + + /** + * Extracts the call to openlog() in order to run unit tests on it. This function + * will initialize the connection to the system logger + * + * @param string $ident the prefix to add to all messages logged + * @param int $options the options flags to be used for logged messages + * @param int $facility the stream or facility to log to + * @return void + */ + protected function _open($ident, $options, $facility) + { + openlog($ident, $options, $facility); + } + + /** + * Extracts the call to syslog() in order to run unit tests on it. This function + * will perform the actual write in the system logger + * + * @param int $priority Message priority. + * @param string $message Message to log. + * @return bool + */ + protected function _write($priority, $message) + { + return syslog($priority, $message); + } + + /** + * Closes the logger connection + */ + public function __destruct() + { + closelog(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Log/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Log/LICENSE.txt new file mode 100644 index 000000000..0c4b7932c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Log/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2016, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/vendor/cakephp/cakephp/src/Log/Log.php b/app/vendor/cakephp/cakephp/src/Log/Log.php new file mode 100644 index 000000000..894c2f03d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Log/Log.php @@ -0,0 +1,519 @@ + 'FileLog']); + * ``` + * + * You can define the className as any fully namespaced classname or use a short hand + * classname to use loggers in the `App\Log\Engine` & `Cake\Log\Engine` namespaces. + * You can also use plugin short hand to use logging classes provided by plugins. + * + * Log adapters are required to implement `Psr\Log\LoggerInterface`, and there is a + * built-in base class (`Cake\Log\Engine\BaseLog`) that can be used for custom loggers. + * + * Outside of the `className` key, all other configuration values will be passed to the + * logging adapter's constructor as an array. + * + * ### Logging levels + * + * When configuring loggers, you can set which levels a logger will handle. + * This allows you to disable debug messages in production for example: + * + * ``` + * Log::setConfig('default', [ + * 'className' => 'File', + * 'path' => LOGS, + * 'levels' => ['error', 'critical', 'alert', 'emergency'] + * ]); + * ``` + * + * The above logger would only log error messages or higher. Any + * other log messages would be discarded. + * + * ### Logging scopes + * + * When configuring loggers you can define the active scopes the logger + * is for. If defined, only the listed scopes will be handled by the + * logger. If you don't define any scopes an adapter will catch + * all scopes that match the handled levels. + * + * ``` + * Log::setConfig('payments', [ + * 'className' => 'File', + * 'scopes' => ['payment', 'order'] + * ]); + * ``` + * + * The above logger will only capture log entries made in the + * `payment` and `order` scopes. All other scopes including the + * undefined scope will be ignored. + * + * ### Writing to the log + * + * You write to the logs using Log::write(). See its documentation for more information. + * + * ### Logging Levels + * + * By default Cake Log supports all the log levels defined in + * RFC 5424. When logging messages you can either use the named methods, + * or the correct constants with `write()`: + * + * ``` + * Log::error('Something horrible happened'); + * Log::write(LOG_ERR, 'Something horrible happened'); + * ``` + * + * ### Logging scopes + * + * When logging messages and configuring log adapters, you can specify + * 'scopes' that the logger will handle. You can think of scopes as subsystems + * in your application that may require different logging setups. For + * example in an e-commerce application you may want to handle logged errors + * in the cart and ordering subsystems differently than the rest of the + * application. By using scopes you can control logging for each part + * of your application and also use standard log levels. + */ +class Log +{ + + use StaticConfigTrait { + setConfig as protected _setConfig; + } + + /** + * An array mapping url schemes to fully qualified Log engine class names + * + * @var array + */ + protected static $_dsnClassMap = [ + 'console' => 'Cake\Log\Engine\ConsoleLog', + 'file' => 'Cake\Log\Engine\FileLog', + 'syslog' => 'Cake\Log\Engine\SyslogLog', + ]; + + /** + * Internal flag for tracking whether or not configuration has been changed. + * + * @var bool + */ + protected static $_dirtyConfig = false; + + /** + * LogEngineRegistry class + * + * @var \Cake\Log\LogEngineRegistry|null + */ + protected static $_registry; + + /** + * Handled log levels + * + * @var array + */ + protected static $_levels = [ + 'emergency', + 'alert', + 'critical', + 'error', + 'warning', + 'notice', + 'info', + 'debug' + ]; + + /** + * Log levels as detailed in RFC 5424 + * https://tools.ietf.org/html/rfc5424 + * + * @var array + */ + protected static $_levelMap = [ + 'emergency' => LOG_EMERG, + 'alert' => LOG_ALERT, + 'critical' => LOG_CRIT, + 'error' => LOG_ERR, + 'warning' => LOG_WARNING, + 'notice' => LOG_NOTICE, + 'info' => LOG_INFO, + 'debug' => LOG_DEBUG, + ]; + + /** + * Initializes registry and configurations + * + * @return void + */ + protected static function _init() + { + if (empty(static::$_registry)) { + static::$_registry = new LogEngineRegistry(); + } + if (static::$_dirtyConfig) { + static::_loadConfig(); + } + static::$_dirtyConfig = false; + } + + /** + * Load the defined configuration and create all the defined logging + * adapters. + * + * @return void + */ + protected static function _loadConfig() + { + foreach (static::$_config as $name => $properties) { + if (isset($properties['engine'])) { + $properties['className'] = $properties['engine']; + } + if (!static::$_registry->has($name)) { + static::$_registry->load($name, $properties); + } + } + } + + /** + * Reset all the connected loggers. This is useful to do when changing the logging + * configuration or during testing when you want to reset the internal state of the + * Log class. + * + * Resets the configured logging adapters, as well as any custom logging levels. + * This will also clear the configuration data. + * + * @return void + */ + public static function reset() + { + static::$_registry = null; + static::$_config = []; + static::$_dirtyConfig = true; + } + + /** + * Gets log levels + * + * Call this method to obtain current + * level configuration. + * + * @return array active log levels + */ + public static function levels() + { + return static::$_levels; + } + + /** + * This method can be used to define logging adapters for an application + * or read existing configuration. + * + * To change an adapter's configuration at runtime, first drop the adapter and then + * reconfigure it. + * + * Loggers will not be constructed until the first log message is written. + * + * ### Usage + * + * Setting a cache engine up. + * + * ``` + * Log::setConfig('default', $settings); + * ``` + * + * Injecting a constructed adapter in: + * + * ``` + * Log::setConfig('default', $instance); + * ``` + * + * Using a factory function to get an adapter: + * + * ``` + * Log::setConfig('default', function () { return new FileLog(); }); + * ``` + * + * Configure multiple adapters at once: + * + * ``` + * Log::setConfig($arrayOfConfig); + * ``` + * + * @param string|array $key The name of the logger config, or an array of multiple configs. + * @param array|null $config An array of name => config data for adapter. + * @return void + * @throws \BadMethodCallException When trying to modify an existing config. + */ + public static function setConfig($key, $config = null) + { + static::_setConfig($key, $config); + static::$_dirtyConfig = true; + } + + /** + * Get a logging engine. + * + * @param string $name Key name of a configured adapter to get. + * @return \Cake\Log\Engine\BaseLog|false Instance of BaseLog or false if not found + */ + public static function engine($name) + { + static::_init(); + if (static::$_registry->{$name}) { + return static::$_registry->{$name}; + } + + return false; + } + + /** + * Writes the given message and type to all of the configured log adapters. + * Configured adapters are passed both the $level and $message variables. $level + * is one of the following strings/values. + * + * ### Levels: + * + * - `LOG_EMERG` => 'emergency', + * - `LOG_ALERT` => 'alert', + * - `LOG_CRIT` => 'critical', + * - `LOG_ERR` => 'error', + * - `LOG_WARNING` => 'warning', + * - `LOG_NOTICE` => 'notice', + * - `LOG_INFO` => 'info', + * - `LOG_DEBUG` => 'debug', + * + * ### Basic usage + * + * Write a 'warning' message to the logs: + * + * ``` + * Log::write('warning', 'Stuff is broken here'); + * ``` + * + * ### Using scopes + * + * When writing a log message you can define one or many scopes for the message. + * This allows you to handle messages differently based on application section/feature. + * + * ``` + * Log::write('warning', 'Payment failed', ['scope' => 'payment']); + * ``` + * + * When configuring loggers you can configure the scopes a particular logger will handle. + * When using scopes, you must ensure that the level of the message, and the scope of the message + * intersect with the defined levels & scopes for a logger. + * + * ### Unhandled log messages + * + * If no configured logger can handle a log message (because of level or scope restrictions) + * then the logged message will be ignored and silently dropped. You can check if this has happened + * by inspecting the return of write(). If false the message was not handled. + * + * @param int|string $level The severity level of the message being written. + * The value must be an integer or string matching a known level. + * @param mixed $message Message content to log + * @param string|array $context Additional data to be used for logging the message. + * The special `scope` key can be passed to be used for further filtering of the + * log engines to be used. If a string or a numerically index array is passed, it + * will be treated as the `scope` key. + * See Cake\Log\Log::setConfig() for more information on logging scopes. + * @return bool Success + * @throws \InvalidArgumentException If invalid level is passed. + */ + public static function write($level, $message, $context = []) + { + static::_init(); + if (is_int($level) && in_array($level, static::$_levelMap)) { + $level = array_search($level, static::$_levelMap); + } + + if (!in_array($level, static::$_levels)) { + throw new InvalidArgumentException(sprintf('Invalid log level "%s"', $level)); + } + + $logged = false; + $context = (array)$context; + if (isset($context[0])) { + $context = ['scope' => $context]; + } + $context += ['scope' => []]; + + foreach (static::$_registry->loaded() as $streamName) { + $logger = static::$_registry->{$streamName}; + $levels = $scopes = null; + + if ($logger instanceof BaseLog) { + $levels = $logger->levels(); + $scopes = $logger->scopes(); + } + if ($scopes === null) { + $scopes = []; + } + + $correctLevel = empty($levels) || in_array($level, $levels); + $inScope = $scopes === false && empty($context['scope']) || $scopes === [] || + is_array($scopes) && array_intersect((array)$context['scope'], $scopes); + + if ($correctLevel && $inScope) { + $logger->log($level, $message, $context); + $logged = true; + } + } + + return $logged; + } + + /** + * Convenience method to log emergency messages + * + * @param string $message log message + * @param string|array $context Additional data to be used for logging the message. + * The special `scope` key can be passed to be used for further filtering of the + * log engines to be used. If a string or a numerically index array is passed, it + * will be treated as the `scope` key. + * See Cake\Log\Log::setConfig() for more information on logging scopes. + * @return bool Success + */ + public static function emergency($message, $context = []) + { + return static::write(__FUNCTION__, $message, $context); + } + + /** + * Convenience method to log alert messages + * + * @param string $message log message + * @param string|array $context Additional data to be used for logging the message. + * The special `scope` key can be passed to be used for further filtering of the + * log engines to be used. If a string or a numerically index array is passed, it + * will be treated as the `scope` key. + * See Cake\Log\Log::setConfig() for more information on logging scopes. + * @return bool Success + */ + public static function alert($message, $context = []) + { + return static::write(__FUNCTION__, $message, $context); + } + + /** + * Convenience method to log critical messages + * + * @param string $message log message + * @param string|array $context Additional data to be used for logging the message. + * The special `scope` key can be passed to be used for further filtering of the + * log engines to be used. If a string or a numerically index array is passed, it + * will be treated as the `scope` key. + * See Cake\Log\Log::setConfig() for more information on logging scopes. + * @return bool Success + */ + public static function critical($message, $context = []) + { + return static::write(__FUNCTION__, $message, $context); + } + + /** + * Convenience method to log error messages + * + * @param string $message log message + * @param string|array $context Additional data to be used for logging the message. + * The special `scope` key can be passed to be used for further filtering of the + * log engines to be used. If a string or a numerically index array is passed, it + * will be treated as the `scope` key. + * See Cake\Log\Log::setConfig() for more information on logging scopes. + * @return bool Success + */ + public static function error($message, $context = []) + { + return static::write(__FUNCTION__, $message, $context); + } + + /** + * Convenience method to log warning messages + * + * @param string $message log message + * @param string|array $context Additional data to be used for logging the message. + * The special `scope` key can be passed to be used for further filtering of the + * log engines to be used. If a string or a numerically index array is passed, it + * will be treated as the `scope` key. + * See Cake\Log\Log::setConfig() for more information on logging scopes. + * @return bool Success + */ + public static function warning($message, $context = []) + { + return static::write(__FUNCTION__, $message, $context); + } + + /** + * Convenience method to log notice messages + * + * @param string $message log message + * @param string|array $context Additional data to be used for logging the message. + * The special `scope` key can be passed to be used for further filtering of the + * log engines to be used. If a string or a numerically index array is passed, it + * will be treated as the `scope` key. + * See Cake\Log\Log::setConfig() for more information on logging scopes. + * @return bool Success + */ + public static function notice($message, $context = []) + { + return static::write(__FUNCTION__, $message, $context); + } + + /** + * Convenience method to log debug messages + * + * @param string $message log message + * @param string|array $context Additional data to be used for logging the message. + * The special `scope` key can be passed to be used for further filtering of the + * log engines to be used. If a string or a numerically index array is passed, it + * will be treated as the `scope` key. + * See Cake\Log\Log::setConfig() for more information on logging scopes. + * @return bool Success + */ + public static function debug($message, $context = []) + { + return static::write(__FUNCTION__, $message, $context); + } + + /** + * Convenience method to log info messages + * + * @param string $message log message + * @param string|array $context Additional data to be used for logging the message. + * The special `scope` key can be passed to be used for further filtering of the + * log engines to be used. If a string or a numerically index array is passed, it + * will be treated as the `scope` key. + * See Cake\Log\Log::setConfig() for more information on logging scopes. + * @return bool Success + */ + public static function info($message, $context = []) + { + return static::write(__FUNCTION__, $message, $context); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Log/LogEngineRegistry.php b/app/vendor/cakephp/cakephp/src/Log/LogEngineRegistry.php new file mode 100644 index 000000000..0fa534f79 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Log/LogEngineRegistry.php @@ -0,0 +1,104 @@ +_loaded[$name]); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Log/LogTrait.php b/app/vendor/cakephp/cakephp/src/Log/LogTrait.php new file mode 100644 index 000000000..31b943e87 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Log/LogTrait.php @@ -0,0 +1,38 @@ + 'FileLog', + 'levels' => ['notice', 'info', 'debug'], + 'file' => '/path/to/file.log', +]); + +// Fully namespaced name. +Log::config('production', [ + 'className' => 'Cake\Log\Engine\SyslogLog', + 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], +]); +``` + +It is also possible to create loggers by providing a closure. + +```php +Log::config('special', function () { + // Return any PSR-3 compatible logger + return new MyPSR3CompatibleLogger(); +}); +``` + +Or by injecting an instance directly: + +```php +Log::config('special', new MyPSR3CompatibleLogger()); +``` + +You can then use the `Log` class to pass messages to the logging backends: + +```php +Log::write('debug', 'Something did not work'); +``` + +Only the logging engines subscribed to the log level you are writing to will +get the message passed. In the example above, only the 'local' engine will get +the log message. + +### Filtering messages with scopes + +The Log library supports another level of message filtering. By using scopes, +you can limit the logging engines that receive a particular message. + +```php +// Configure /logs/payments.log to receive all levels, but only +// those with `payments` scope. +Log::config('payments', [ + 'className' => 'FileLog', + 'levels' => ['error', 'info', 'warning'], + 'scopes' => ['payments'], + 'file' => '/logs/payments.log', +]); + +Log::warning('this gets written only to payments.log', ['scope' => ['payments']]); +``` + +## Documentation + +Please make sure you check the [official documentation](https://book.cakephp.org/3.0/en/core-libraries/logging.html) diff --git a/app/vendor/cakephp/cakephp/src/Log/composer.json b/app/vendor/cakephp/cakephp/src/Log/composer.json new file mode 100644 index 000000000..a775a4f9f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Log/composer.json @@ -0,0 +1,35 @@ +{ + "name": "cakephp/log", + "description": "CakePHP logging library with support for multiple different streams", + "type": "library", + "keywords": [ + "cakephp", + "log", + "logging", + "streams" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/log/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/log" + }, + "require": { + "php": ">=5.6.0", + "cakephp/core": "^3.6.0", + "psr/log": "^1.0.0" + }, + "autoload": { + "psr-4": { + "Cake\\Log\\": "." + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Mailer/AbstractTransport.php b/app/vendor/cakephp/cakephp/src/Mailer/AbstractTransport.php new file mode 100644 index 000000000..908b15d29 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Mailer/AbstractTransport.php @@ -0,0 +1,76 @@ +setConfig($config); + } + + /** + * Help to convert headers in string + * + * @param array $headers Headers in format key => value + * @param string $eol End of line string. + * @return string + */ + protected function _headersToString($headers, $eol = "\r\n") + { + $out = ''; + foreach ($headers as $key => $value) { + if ($value === false || $value === null || $value === '') { + continue; + } + $out .= $key . ': ' . $value . $eol; + } + if (!empty($out)) { + $out = substr($out, 0, -1 * strlen($eol)); + } + + return $out; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Mailer/Email.php b/app/vendor/cakephp/cakephp/src/Mailer/Email.php new file mode 100644 index 000000000..87c16dcae --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Mailer/Email.php @@ -0,0 +1,2923 @@ + 'Cake\Mailer\Transport\DebugTransport', + 'mail' => 'Cake\Mailer\Transport\MailTransport', + 'smtp' => 'Cake\Mailer\Transport\SmtpTransport', + ]; + + /** + * Configuration profiles for transports. + * + * @var array + */ + protected static $_transportConfig = []; + + /** + * A copy of the configuration profile for this + * instance. This copy can be modified with Email::profile(). + * + * @var array + */ + protected $_profile = []; + + /** + * 8Bit character sets + * + * @var array + */ + protected $_charset8bit = ['UTF-8', 'SHIFT_JIS']; + + /** + * Define Content-Type charset name + * + * @var array + */ + protected $_contentTypeCharset = [ + 'ISO-2022-JP-MS' => 'ISO-2022-JP' + ]; + + /** + * Regex for email validation + * + * If null, filter_var() will be used. Use the emailPattern() method + * to set a custom pattern.' + * + * @var string + */ + protected $_emailPattern = self::EMAIL_PATTERN; + + /** + * Constructor + * + * @param array|string|null $config Array of configs, or string to load configs from app.php + */ + public function __construct($config = null) + { + $this->_appCharset = Configure::read('App.encoding'); + if ($this->_appCharset !== null) { + $this->charset = $this->_appCharset; + } + $this->_domain = preg_replace('/\:\d+$/', '', env('HTTP_HOST')); + if (empty($this->_domain)) { + $this->_domain = php_uname('n'); + } + + $this->viewBuilder() + ->setClassName('Cake\View\View') + ->setTemplate('') + ->setLayout('default') + ->setHelpers(['Html']); + + if ($config === null) { + $config = static::getConfig('default'); + } + if ($config) { + $this->setProfile($config); + } + if (empty($this->headerCharset)) { + $this->headerCharset = $this->charset; + } + } + + /** + * Clone ViewBuilder instance when email object is cloned. + * + * @return void + */ + public function __clone() + { + $this->_viewBuilder = clone $this->viewBuilder(); + } + + /** + * Sets "from" address. + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return $this + * @throws \InvalidArgumentException + */ + public function setFrom($email, $name = null) + { + return $this->_setEmailSingle('_from', $email, $name, 'From requires only 1 email address.'); + } + + /** + * Gets "from" address. + * + * @return array + */ + public function getFrom() + { + return $this->_from; + } + + /** + * From + * + * @deprecated 3.4.0 Use setFrom()/getFrom() instead. + * @param string|array|null $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return array|$this + * @throws \InvalidArgumentException + */ + public function from($email = null, $name = null) + { + deprecationWarning('Email::from() is deprecated. Use Email::setFrom() or Email::getFrom() instead.'); + if ($email === null) { + return $this->getFrom(); + } + + return $this->setFrom($email, $name); + } + + /** + * Sets "sender" address. + * + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return $this + * @throws \InvalidArgumentException + */ + public function setSender($email, $name = null) + { + return $this->_setEmailSingle('_sender', $email, $name, 'Sender requires only 1 email address.'); + } + + /** + * Gets "sender" address. + * + * @return array + */ + public function getSender() + { + return $this->_sender; + } + + /** + * Sender + * + * @deprecated 3.4.0 Use setSender()/getSender() instead. + * @param string|array|null $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return array|$this + * @throws \InvalidArgumentException + */ + public function sender($email = null, $name = null) + { + deprecationWarning('Email::sender() is deprecated. Use Email::setSender() or Email::getSender() instead.'); + + if ($email === null) { + return $this->getSender(); + } + + return $this->setSender($email, $name); + } + + /** + * Sets "Reply-To" address. + * + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return $this + * @throws \InvalidArgumentException + */ + public function setReplyTo($email, $name = null) + { + return $this->_setEmailSingle('_replyTo', $email, $name, 'Reply-To requires only 1 email address.'); + } + + /** + * Gets "Reply-To" address. + * + * @return array + */ + public function getReplyTo() + { + return $this->_replyTo; + } + + /** + * Reply-To + * + * @deprecated 3.4.0 Use setReplyTo()/getReplyTo() instead. + * @param string|array|null $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return array|$this + * @throws \InvalidArgumentException + */ + public function replyTo($email = null, $name = null) + { + deprecationWarning('Email::replyTo() is deprecated. Use Email::setReplyTo() or Email::getReplyTo() instead.'); + + if ($email === null) { + return $this->getReplyTo(); + } + + return $this->setReplyTo($email, $name); + } + + /** + * Sets Read Receipt (Disposition-Notification-To header). + * + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return $this + * @throws \InvalidArgumentException + */ + public function setReadReceipt($email, $name = null) + { + return $this->_setEmailSingle('_readReceipt', $email, $name, 'Disposition-Notification-To requires only 1 email address.'); + } + + /** + * Gets Read Receipt (Disposition-Notification-To header). + * + * @return array + */ + public function getReadReceipt() + { + return $this->_readReceipt; + } + + /** + * Read Receipt (Disposition-Notification-To header) + * + * @deprecated 3.4.0 Use setReadReceipt()/getReadReceipt() instead. + * @param string|array|null $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return array|$this + * @throws \InvalidArgumentException + */ + public function readReceipt($email = null, $name = null) + { + deprecationWarning('Email::readReceipt() is deprecated. Use Email::setReadReceipt() or Email::getReadReceipt() instead.'); + + if ($email === null) { + return $this->getReadReceipt(); + } + + return $this->setReadReceipt($email, $name); + } + + /** + * Return Path + * + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return $this + * @throws \InvalidArgumentException + */ + public function setReturnPath($email, $name = null) + { + return $this->_setEmailSingle('_returnPath', $email, $name, 'Return-Path requires only 1 email address.'); + } + + /** + * Gets return path. + * + * @return array + */ + public function getReturnPath() + { + return $this->_returnPath; + } + + /** + * Return Path + * + * @deprecated 3.4.0 Use setReturnPath()/getReturnPath() instead. + * @param string|array|null $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return array|$this + * @throws \InvalidArgumentException + */ + public function returnPath($email = null, $name = null) + { + deprecationWarning('Email::returnPath() is deprecated. Use Email::setReturnPath() or Email::getReturnPath() instead.'); + if ($email === null) { + return $this->getReturnPath(); + } + + return $this->setReturnPath($email, $name); + } + + /** + * Sets "to" address. + * + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return $this + */ + public function setTo($email, $name = null) + { + return $this->_setEmail('_to', $email, $name); + } + + /** + * Gets "to" address + * + * @return array + */ + public function getTo() + { + return $this->_to; + } + + /** + * To + * + * @deprecated 3.4.0 Use setTo()/getTo() instead. + * @param string|array|null $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return array|$this + */ + public function to($email = null, $name = null) + { + deprecationWarning('Email::to() is deprecated. Use Email::setTo() or Email::getTo() instead.'); + + if ($email === null) { + return $this->getTo(); + } + + return $this->setTo($email, $name); + } + + /** + * Add To + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return $this + */ + public function addTo($email, $name = null) + { + return $this->_addEmail('_to', $email, $name); + } + + /** + * Sets "cc" address. + * + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return $this + */ + public function setCc($email, $name = null) + { + return $this->_setEmail('_cc', $email, $name); + } + + /** + * Gets "cc" address. + * + * @return array + */ + public function getCc() + { + return $this->_cc; + } + + /** + * Cc + * + * @deprecated 3.4.0 Use setCc()/getCc() instead. + * @param string|array|null $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return array|$this + */ + public function cc($email = null, $name = null) + { + deprecationWarning('Email::cc() is deprecated. Use Email::setCc() or Email::getCc() instead.'); + + if ($email === null) { + return $this->getCc(); + } + + return $this->setCc($email, $name); + } + + /** + * Add Cc + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return $this + */ + public function addCc($email, $name = null) + { + return $this->_addEmail('_cc', $email, $name); + } + + /** + * Sets "bcc" address. + * + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return $this + */ + public function setBcc($email, $name = null) + { + return $this->_setEmail('_bcc', $email, $name); + } + + /** + * Gets "bcc" address. + * + * @return array + */ + public function getBcc() + { + return $this->_bcc; + } + + /** + * Bcc + * + * @deprecated 3.4.0 Use setBcc()/getBcc() instead. + * @param string|array|null $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return array|$this + */ + public function bcc($email = null, $name = null) + { + deprecationWarning('Email::bcc() is deprecated. Use Email::setBcc() or Email::getBcc() instead.'); + + if ($email === null) { + return $this->getBcc(); + } + + return $this->setBcc($email, $name); + } + + /** + * Add Bcc + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string|null $name Name + * @return $this + */ + public function addBcc($email, $name = null) + { + return $this->_addEmail('_bcc', $email, $name); + } + + /** + * Charset setter. + * + * @param string|null $charset Character set. + * @return $this + */ + public function setCharset($charset) + { + $this->charset = $charset; + if (!$this->headerCharset) { + $this->headerCharset = $charset; + } + + return $this; + } + + /** + * Charset getter. + * + * @return string Charset + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Charset setter/getter + * + * @deprecated 3.4.0 Use setCharset()/getCharset() instead. + * @param string|null $charset Character set. + * @return string Charset + */ + public function charset($charset = null) + { + deprecationWarning('Email::charset() is deprecated. Use Email::setCharset() or Email::getCharset() instead.'); + + if ($charset === null) { + return $this->getCharset(); + } + $this->setCharset($charset); + + return $this->charset; + } + + /** + * HeaderCharset setter. + * + * @param string|null $charset Character set. + * @return $this + */ + public function setHeaderCharset($charset) + { + $this->headerCharset = $charset; + + return $this; + } + + /** + * HeaderCharset getter. + * + * @return string Charset + */ + public function getHeaderCharset() + { + return $this->headerCharset; + } + + /** + * HeaderCharset setter/getter + * + * @deprecated 3.4.0 Use setHeaderCharset()/getHeaderCharset() instead. + * @param string|null $charset Character set. + * @return string Charset + */ + public function headerCharset($charset = null) + { + deprecationWarning('Email::headerCharset() is deprecated. Use Email::setHeaderCharset() or Email::getHeaderCharset() instead.'); + + if ($charset === null) { + return $this->getHeaderCharset(); + } + + $this->setHeaderCharset($charset); + + return $this->headerCharset; + } + + /** + * TransferEncoding setter. + * + * @param string|null $encoding Encoding set. + * @return $this + */ + public function setTransferEncoding($encoding) + { + $encoding = strtolower($encoding); + if (!in_array($encoding, $this->_transferEncodingAvailable)) { + throw new InvalidArgumentException( + sprintf( + 'Transfer encoding not available. Can be : %s.', + implode(', ', $this->_transferEncodingAvailable) + ) + ); + } + $this->transferEncoding = $encoding; + + return $this; + } + + /** + * TransferEncoding getter. + * + * @return string|null Encoding + */ + public function getTransferEncoding() + { + return $this->transferEncoding; + } + + /** + * EmailPattern setter/getter + * + * @param string|null $regex The pattern to use for email address validation, + * null to unset the pattern and make use of filter_var() instead. + * @return $this + */ + public function setEmailPattern($regex) + { + $this->_emailPattern = $regex; + + return $this; + } + + /** + * EmailPattern setter/getter + * + * @return string + */ + public function getEmailPattern() + { + return $this->_emailPattern; + } + + /** + * EmailPattern setter/getter + * + * @deprecated 3.4.0 Use setEmailPattern()/getEmailPattern() instead. + * @param string|bool|null $regex The pattern to use for email address validation, + * null to unset the pattern and make use of filter_var() instead, false or + * nothing to return the current value + * @return string|$this + */ + public function emailPattern($regex = false) + { + deprecationWarning('Email::emailPattern() is deprecated. Use Email::setEmailPattern() or Email::getEmailPattern() instead.'); + + if ($regex === false) { + return $this->getEmailPattern(); + } + + return $this->setEmailPattern($regex); + } + + /** + * Set email + * + * @param string $varName Property name + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return $this + * @throws \InvalidArgumentException + */ + protected function _setEmail($varName, $email, $name) + { + if (!is_array($email)) { + $this->_validateEmail($email, $varName); + if ($name === null) { + $name = $email; + } + $this->{$varName} = [$email => $name]; + + return $this; + } + $list = []; + foreach ($email as $key => $value) { + if (is_int($key)) { + $key = $value; + } + $this->_validateEmail($key, $varName); + $list[$key] = $value; + } + $this->{$varName} = $list; + + return $this; + } + + /** + * Validate email address + * + * @param string $email Email address to validate + * @param string $context Which property was set + * @return void + * @throws \InvalidArgumentException If email address does not validate + */ + protected function _validateEmail($email, $context) + { + if ($this->_emailPattern === null) { + if (filter_var($email, FILTER_VALIDATE_EMAIL)) { + return; + } + } elseif (preg_match($this->_emailPattern, $email)) { + return; + } + + $context = ltrim($context, '_'); + if ($email == '') { + throw new InvalidArgumentException(sprintf('The email set for "%s" is empty.', $context)); + } + throw new InvalidArgumentException(sprintf('Invalid email set for "%s". You passed "%s".', $context, $email)); + } + + /** + * Set only 1 email + * + * @param string $varName Property name + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @param string $throwMessage Exception message + * @return $this + * @throws \InvalidArgumentException + */ + protected function _setEmailSingle($varName, $email, $name, $throwMessage) + { + if ($email === []) { + $this->{$varName} = $email; + + return $this; + } + + $current = $this->{$varName}; + $this->_setEmail($varName, $email, $name); + if (count($this->{$varName}) !== 1) { + $this->{$varName} = $current; + throw new InvalidArgumentException($throwMessage); + } + + return $this; + } + + /** + * Add email + * + * @param string $varName Property name + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return $this + * @throws \InvalidArgumentException + */ + protected function _addEmail($varName, $email, $name) + { + if (!is_array($email)) { + $this->_validateEmail($email, $varName); + if ($name === null) { + $name = $email; + } + $this->{$varName}[$email] = $name; + + return $this; + } + $list = []; + foreach ($email as $key => $value) { + if (is_int($key)) { + $key = $value; + } + $this->_validateEmail($key, $varName); + $list[$key] = $value; + } + $this->{$varName} = array_merge($this->{$varName}, $list); + + return $this; + } + + /** + * Sets subject. + * + * @param string $subject Subject string. + * @return $this + */ + public function setSubject($subject) + { + $this->_subject = $this->_encode((string)$subject); + + return $this; + } + + /** + * Gets subject. + * + * @return string + */ + public function getSubject() + { + return $this->_subject; + } + + /** + * Get/Set Subject. + * + * @deprecated 3.4.0 Use setSubject()/getSubject() instead. + * @param string|null $subject Subject string. + * @return string|$this + */ + public function subject($subject = null) + { + deprecationWarning('Email::subject() is deprecated. Use Email::setSubject() or Email::getSubject() instead.'); + + if ($subject === null) { + return $this->getSubject(); + } + + return $this->setSubject($subject); + } + + /** + * Get original subject without encoding + * + * @return string Original subject + */ + public function getOriginalSubject() + { + return $this->_decode($this->_subject); + } + + /** + * Sets headers for the message + * + * @param array $headers Associative array containing headers to be set. + * @return $this + */ + public function setHeaders(array $headers) + { + $this->_headers = $headers; + + return $this; + } + + /** + * Add header for the message + * + * @param array $headers Headers to set. + * @return $this + */ + public function addHeaders(array $headers) + { + $this->_headers = array_merge($this->_headers, $headers); + + return $this; + } + + /** + * Get list of headers + * + * ### Includes: + * + * - `from` + * - `replyTo` + * - `readReceipt` + * - `returnPath` + * - `to` + * - `cc` + * - `bcc` + * - `subject` + * + * @param array $include List of headers. + * @return array + */ + public function getHeaders(array $include = []) + { + if ($include == array_values($include)) { + $include = array_fill_keys($include, true); + } + $defaults = array_fill_keys( + [ + 'from', 'sender', 'replyTo', 'readReceipt', 'returnPath', + 'to', 'cc', 'bcc', 'subject'], + false + ); + $include += $defaults; + + $headers = []; + $relation = [ + 'from' => 'From', + 'replyTo' => 'Reply-To', + 'readReceipt' => 'Disposition-Notification-To', + 'returnPath' => 'Return-Path' + ]; + foreach ($relation as $var => $header) { + if ($include[$var]) { + $var = '_' . $var; + $headers[$header] = current($this->_formatAddress($this->{$var})); + } + } + if ($include['sender']) { + if (key($this->_sender) === key($this->_from)) { + $headers['Sender'] = ''; + } else { + $headers['Sender'] = current($this->_formatAddress($this->_sender)); + } + } + + foreach (['to', 'cc', 'bcc'] as $var) { + if ($include[$var]) { + $classVar = '_' . $var; + $headers[ucfirst($var)] = implode(', ', $this->_formatAddress($this->{$classVar})); + } + } + + $headers += $this->_headers; + if (!isset($headers['Date'])) { + $headers['Date'] = date(DATE_RFC2822); + } + if ($this->_messageId !== false) { + if ($this->_messageId === true) { + $headers['Message-ID'] = '<' . str_replace('-', '', Text::uuid()) . '@' . $this->_domain . '>'; + } else { + $headers['Message-ID'] = $this->_messageId; + } + } + + if ($this->_priority) { + $headers['X-Priority'] = $this->_priority; + } + + if ($include['subject']) { + $headers['Subject'] = $this->_subject; + } + + $headers['MIME-Version'] = '1.0'; + if ($this->_attachments) { + $headers['Content-Type'] = 'multipart/mixed; boundary="' . $this->_boundary . '"'; + } elseif ($this->_emailFormat === 'both') { + $headers['Content-Type'] = 'multipart/alternative; boundary="' . $this->_boundary . '"'; + } elseif ($this->_emailFormat === 'text') { + $headers['Content-Type'] = 'text/plain; charset=' . $this->_getContentTypeCharset(); + } elseif ($this->_emailFormat === 'html') { + $headers['Content-Type'] = 'text/html; charset=' . $this->_getContentTypeCharset(); + } + $headers['Content-Transfer-Encoding'] = $this->_getContentTransferEncoding(); + + return $headers; + } + + /** + * Format addresses + * + * If the address contains non alphanumeric/whitespace characters, it will + * be quoted as characters like `:` and `,` are known to cause issues + * in address header fields. + * + * @param array $address Addresses to format. + * @return array + */ + protected function _formatAddress($address) + { + $return = []; + foreach ($address as $email => $alias) { + if ($email === $alias) { + $return[] = $email; + } else { + $encoded = $this->_encode($alias); + if ($encoded === $alias && preg_match('/[^a-z0-9 ]/i', $encoded)) { + $encoded = '"' . str_replace('"', '\"', $encoded) . '"'; + } + $return[] = sprintf('%s <%s>', $encoded, $email); + } + } + + return $return; + } + + /** + * Sets template. + * + * @param string|null $template Template name or null to not use. + * @return $this + */ + public function setTemplate($template) + { + $this->viewBuilder()->setTemplate($template ?: ''); + + return $this; + } + + /** + * Gets template. + * + * @return string + */ + public function getTemplate() + { + return $this->viewBuilder()->getTemplate(); + } + + /** + * Sets layout. + * + * @param string|null $layout Layout name or null to not use + * @return $this + */ + public function setLayout($layout) + { + $this->viewBuilder()->setLayout($layout ?: false); + + return $this; + } + + /** + * Gets layout. + * + * @return string + */ + public function getLayout() + { + return $this->viewBuilder()->getLayout(); + } + + /** + * Template and layout + * + * @deprecated 3.4.0 Use setTemplate()/getTemplate() and setLayout()/getLayout() instead. + * @param bool|string $template Template name or null to not use + * @param bool|string $layout Layout name or null to not use + * @return array|$this + */ + public function template($template = false, $layout = false) + { + deprecationWarning('Email::template() is deprecated. Use Email::setTemplate() or Email::getTemplate() and Email::setLayout() or Email::getLayout() instead.'); + + if ($template === false) { + return [ + 'template' => $this->getTemplate(), + 'layout' => $this->getLayout() + ]; + } + $this->setTemplate($template); + if ($layout !== false) { + $this->setLayout($layout); + } + + return $this; + } + + /** + * Sets view class for render. + * + * @param string $viewClass View class name. + * @return $this + */ + public function setViewRenderer($viewClass) + { + $this->viewBuilder()->setClassName($viewClass); + + return $this; + } + + /** + * Gets view class for render. + * + * @return string + */ + public function getViewRenderer() + { + return $this->viewBuilder()->getClassName(); + } + + /** + * View class for render + * + * @deprecated 3.4.0 Use setViewRenderer()/getViewRenderer() instead. + * @param string|null $viewClass View class name. + * @return string|$this + */ + public function viewRender($viewClass = null) + { + deprecationWarning('Email::viewRender() is deprecated. Use Email::setViewRenderer() or Email::getViewRenderer() instead.'); + + if ($viewClass === null) { + return $this->getViewRenderer(); + } + $this->setViewRenderer($viewClass); + + return $this; + } + + /** + * Sets variables to be set on render. + * + * @param array $viewVars Variables to set for view. + * @return $this + */ + public function setViewVars($viewVars) + { + $this->set((array)$viewVars); + + return $this; + } + + /** + * Gets variables to be set on render. + * + * @return array + */ + public function getViewVars() + { + return $this->viewVars; + } + + /** + * Variables to be set on render + * + * @deprecated 3.4.0 Use setViewVars()/getViewVars() instead. + * @param array|null $viewVars Variables to set for view. + * @return array|$this + */ + public function viewVars($viewVars = null) + { + deprecationWarning('Email::viewVars() is deprecated. Use Email::setViewVars() or Email::getViewVars() instead.'); + + if ($viewVars === null) { + return $this->getViewVars(); + } + + return $this->setViewVars($viewVars); + } + + /** + * Sets theme to use when rendering. + * + * @param string $theme Theme name. + * @return $this + */ + public function setTheme($theme) + { + $this->viewBuilder()->setTheme($theme); + + return $this; + } + + /** + * Gets theme to use when rendering. + * + * @return string + */ + public function getTheme() + { + return $this->viewBuilder()->getTheme(); + } + + /** + * Theme to use when rendering + * + * @deprecated 3.4.0 Use setTheme()/getTheme() instead. + * @param string|null $theme Theme name. + * @return string|$this + */ + public function theme($theme = null) + { + deprecationWarning('Email::theme() is deprecated. Use Email::setTheme() or Email::getTheme() instead.'); + + if ($theme === null) { + return $this->getTheme(); + } + + return $this->setTheme($theme); + } + + /** + * Sets helpers to be used when rendering. + * + * @param array $helpers Helpers list. + * @return $this + */ + public function setHelpers(array $helpers) + { + $this->viewBuilder()->setHelpers($helpers, false); + + return $this; + } + + /** + * Gets helpers to be used when rendering. + * + * @return array + */ + public function getHelpers() + { + return $this->viewBuilder()->getHelpers(); + } + + /** + * Helpers to be used in render + * + * @deprecated 3.4.0 Use setHelpers()/getHelpers() instead. + * @param array|null $helpers Helpers list. + * @return array|$this + */ + public function helpers($helpers = null) + { + deprecationWarning('Email::helpers() is deprecated. Use Email::setHelpers() or Email::getHelpers() instead.'); + + if ($helpers === null) { + return $this->getHelpers(); + } + + return $this->setHelpers((array)$helpers); + } + + /** + * Sets email format. + * + * @param string $format Formatting string. + * @return $this + * @throws \InvalidArgumentException + */ + public function setEmailFormat($format) + { + if (!in_array($format, $this->_emailFormatAvailable)) { + throw new InvalidArgumentException('Format not available.'); + } + $this->_emailFormat = $format; + + return $this; + } + + /** + * Gets email format. + * + * @return string + */ + public function getEmailFormat() + { + return $this->_emailFormat; + } + + /** + * Email format + * + * @deprecated 3.4.0 Use setEmailFormat()/getEmailFormat() instead. + * @param string|null $format Formatting string. + * @return string|$this + * @throws \InvalidArgumentException + */ + public function emailFormat($format = null) + { + deprecationWarning('Email::emailFormat() is deprecated. Use Email::setEmailFormat() or Email::getEmailFormat() instead.'); + + if ($format === null) { + return $this->getEmailFormat(); + } + + return $this->setEmailFormat($format); + } + + /** + * Sets the transport. + * + * When setting the transport you can either use the name + * of a configured transport or supply a constructed transport. + * + * @param string|\Cake\Mailer\AbstractTransport $name Either the name of a configured + * transport, or a transport instance. + * @return $this + * @throws \LogicException When the chosen transport lacks a send method. + * @throws \InvalidArgumentException When $name is neither a string nor an object. + */ + public function setTransport($name) + { + if (is_string($name)) { + $transport = $this->_constructTransport($name); + } elseif (is_object($name)) { + $transport = $name; + } else { + throw new InvalidArgumentException( + sprintf('The value passed for the "$name" argument must be either a string, or an object, %s given.', gettype($name)) + ); + } + if (!method_exists($transport, 'send')) { + throw new LogicException(sprintf('The "%s" do not have send method.', get_class($transport))); + } + + $this->_transport = $transport; + + return $this; + } + + /** + * Gets the transport. + * + * @return \Cake\Mailer\AbstractTransport + */ + public function getTransport() + { + return $this->_transport; + } + + /** + * Get/set the transport. + * + * When setting the transport you can either use the name + * of a configured transport or supply a constructed transport. + * + * @deprecated 3.4.0 Use setTransport()/getTransport() instead. + * @param string|\Cake\Mailer\AbstractTransport|null $name Either the name of a configured + * transport, or a transport instance. + * @return \Cake\Mailer\AbstractTransport|$this + * @throws \LogicException When the chosen transport lacks a send method. + * @throws \InvalidArgumentException When $name is neither a string nor an object. + */ + public function transport($name = null) + { + deprecationWarning('Email::transport() is deprecated. Use Email::setTransport() or Email::getTransport() instead.'); + + if ($name === null) { + return $this->getTransport(); + } + + return $this->setTransport($name); + } + + /** + * Build a transport instance from configuration data. + * + * @param string $name The transport configuration name to build. + * @return \Cake\Mailer\AbstractTransport + * @throws \InvalidArgumentException When transport configuration is missing or invalid. + */ + protected function _constructTransport($name) + { + if (!isset(static::$_transportConfig[$name])) { + throw new InvalidArgumentException(sprintf('Transport config "%s" is missing.', $name)); + } + + if (!isset(static::$_transportConfig[$name]['className'])) { + throw new InvalidArgumentException( + sprintf('Transport config "%s" is invalid, the required `className` option is missing', $name) + ); + } + + $config = static::$_transportConfig[$name]; + + if (is_object($config['className'])) { + if (!$config['className'] instanceof AbstractTransport) { + throw new InvalidArgumentException(sprintf( + 'Transport object must be of type "AbstractTransport". Found invalid type: "%s".', + get_class($config['className']) + )); + } + + return $config['className']; + } + + $className = App::className($config['className'], 'Mailer/Transport', 'Transport'); + if (!$className) { + $className = App::className($config['className'], 'Network/Email', 'Transport'); + if ($className) { + trigger_error( + 'Transports in "Network/Email" are deprecated, use "Mailer/Transport" instead.', + E_USER_DEPRECATED + ); + } + } + + if (!$className) { + throw new InvalidArgumentException(sprintf('Transport class "%s" not found.', $config['className'])); + } + if (!method_exists($className, 'send')) { + throw new InvalidArgumentException(sprintf('The "%s" does not have a send() method.', $className)); + } + + unset($config['className']); + + return new $className($config); + } + + /** + * Sets message ID. + * + * @param bool|string $message True to generate a new Message-ID, False to ignore (not send in email), String to set as Message-ID. + * @return $this + * @throws \InvalidArgumentException + */ + public function setMessageId($message) + { + if (is_bool($message)) { + $this->_messageId = $message; + } else { + if (!preg_match('/^\<.+@.+\>$/', $message)) { + throw new InvalidArgumentException('Invalid format to Message-ID. The text should be something like ""'); + } + $this->_messageId = $message; + } + + return $this; + } + + /** + * Gets message ID. + * + * @return bool|string + */ + public function getMessageId() + { + return $this->_messageId; + } + + /** + * Message-ID + * + * @deprecated 3.4.0 Use setMessageId()/getMessageId() instead. + * @param bool|string|null $message True to generate a new Message-ID, False to ignore (not send in email), String to set as Message-ID + * @return bool|string|$this + * @throws \InvalidArgumentException + */ + public function messageId($message = null) + { + deprecationWarning('Email::messageId() is deprecated. Use Email::setMessageId() or Email::getMessageId() instead.'); + + if ($message === null) { + return $this->getMessageId(); + } + + return $this->setMessageId($message); + } + + /** + * Sets domain. + * + * Domain as top level (the part after @). + * + * @param string $domain Manually set the domain for CLI mailing. + * @return $this + */ + public function setDomain($domain) + { + $this->_domain = $domain; + + return $this; + } + + /** + * Gets domain. + * + * @return string + */ + public function getDomain() + { + return $this->_domain; + } + + /** + * Domain as top level (the part after @) + * + * @deprecated 3.4.0 Use setDomain()/getDomain() instead. + * @param string|null $domain Manually set the domain for CLI mailing + * @return string|$this + */ + public function domain($domain = null) + { + deprecationWarning('Email::domain() is deprecated. Use Email::setDomain() or Email::getDomain() instead.'); + + if ($domain === null) { + return $this->getDomain(); + } + + return $this->setDomain($domain); + } + + /** + * Add attachments to the email message + * + * Attachments can be defined in a few forms depending on how much control you need: + * + * Attach a single file: + * + * ``` + * $email->setAttachments('path/to/file'); + * ``` + * + * Attach a file with a different filename: + * + * ``` + * $email->setAttachments(['custom_name.txt' => 'path/to/file.txt']); + * ``` + * + * Attach a file and specify additional properties: + * + * ``` + * $email->setAttachments(['custom_name.png' => [ + * 'file' => 'path/to/file', + * 'mimetype' => 'image/png', + * 'contentId' => 'abc123', + * 'contentDisposition' => false + * ] + * ]); + * ``` + * + * Attach a file from string and specify additional properties: + * + * ``` + * $email->setAttachments(['custom_name.png' => [ + * 'data' => file_get_contents('path/to/file'), + * 'mimetype' => 'image/png' + * ] + * ]); + * ``` + * + * The `contentId` key allows you to specify an inline attachment. In your email text, you + * can use `` to display the image inline. + * + * The `contentDisposition` key allows you to disable the `Content-Disposition` header, this can improve + * attachment compatibility with outlook email clients. + * + * @param string|array $attachments String with the filename or array with filenames + * @return $this + * @throws \InvalidArgumentException + */ + public function setAttachments($attachments) + { + $attach = []; + foreach ((array)$attachments as $name => $fileInfo) { + if (!is_array($fileInfo)) { + $fileInfo = ['file' => $fileInfo]; + } + if (!isset($fileInfo['file'])) { + if (!isset($fileInfo['data'])) { + throw new InvalidArgumentException('No file or data specified.'); + } + if (is_int($name)) { + throw new InvalidArgumentException('No filename specified.'); + } + $fileInfo['data'] = chunk_split(base64_encode($fileInfo['data']), 76, "\r\n"); + } else { + $fileName = $fileInfo['file']; + $fileInfo['file'] = realpath($fileInfo['file']); + if ($fileInfo['file'] === false || !file_exists($fileInfo['file'])) { + throw new InvalidArgumentException(sprintf('File not found: "%s"', $fileName)); + } + if (is_int($name)) { + $name = basename($fileInfo['file']); + } + } + if (!isset($fileInfo['mimetype']) && isset($fileInfo['file']) && function_exists('mime_content_type')) { + $fileInfo['mimetype'] = mime_content_type($fileInfo['file']); + } + if (!isset($fileInfo['mimetype'])) { + $fileInfo['mimetype'] = 'application/octet-stream'; + } + $attach[$name] = $fileInfo; + } + $this->_attachments = $attach; + + return $this; + } + + /** + * Gets attachments to the email message. + * + * @return array Array of attachments. + */ + public function getAttachments() + { + return $this->_attachments; + } + + /** + * Add attachments to the email message + * + * Attachments can be defined in a few forms depending on how much control you need: + * + * Attach a single file: + * + * ``` + * $email->setAttachments('path/to/file'); + * ``` + * + * Attach a file with a different filename: + * + * ``` + * $email->setAttachments(['custom_name.txt' => 'path/to/file.txt']); + * ``` + * + * Attach a file and specify additional properties: + * + * ``` + * $email->setAttachments(['custom_name.png' => [ + * 'file' => 'path/to/file', + * 'mimetype' => 'image/png', + * 'contentId' => 'abc123', + * 'contentDisposition' => false + * ] + * ]); + * ``` + * + * Attach a file from string and specify additional properties: + * + * ``` + * $email->setAttachments(['custom_name.png' => [ + * 'data' => file_get_contents('path/to/file'), + * 'mimetype' => 'image/png' + * ] + * ]); + * ``` + * + * The `contentId` key allows you to specify an inline attachment. In your email text, you + * can use `` to display the image inline. + * + * The `contentDisposition` key allows you to disable the `Content-Disposition` header, this can improve + * attachment compatibility with outlook email clients. + * + * @deprecated 3.4.0 Use setAttachments()/getAttachments() instead. + * @param string|array|null $attachments String with the filename or array with filenames + * @return array|$this Either the array of attachments when getting or $this when setting. + * @throws \InvalidArgumentException + */ + public function attachments($attachments = null) + { + deprecationWarning('Email::attachments() is deprecated. Use Email::setAttachments() or Email::getAttachments() instead.'); + + if ($attachments === null) { + return $this->getAttachments(); + } + + return $this->setAttachments($attachments); + } + + /** + * Add attachments + * + * @param string|array $attachments String with the filename or array with filenames + * @return $this + * @throws \InvalidArgumentException + * @see \Cake\Mailer\Email::attachments() + */ + public function addAttachments($attachments) + { + $current = $this->_attachments; + $this->setAttachments($attachments); + $this->_attachments = array_merge($current, $this->_attachments); + + return $this; + } + + /** + * Get generated message (used by transport classes) + * + * @param string|null $type Use MESSAGE_* constants or null to return the full message as array + * @return string|array String if type is given, array if type is null + */ + public function message($type = null) + { + switch ($type) { + case static::MESSAGE_HTML: + return $this->_htmlMessage; + case static::MESSAGE_TEXT: + return $this->_textMessage; + } + + return $this->_message; + } + + /** + * Sets priority. + * + * @param int|null $priority 1 (highest) to 5 (lowest) + * @return $this + */ + public function setPriority($priority) + { + $this->_priority = $priority; + + return $this; + } + + /** + * Gets priority. + * + * @return int + */ + public function getPriority() + { + return $this->_priority; + } + + /** + * Sets transport configuration. + * + * Use this method to define transports to use in delivery profiles. + * Once defined you cannot edit the configurations, and must use + * Email::dropTransport() to flush the configuration first. + * + * When using an array of configuration data a new transport + * will be constructed for each message sent. When using a Closure, the + * closure will be evaluated for each message. + * + * The `className` is used to define the class to use for a transport. + * It can either be a short name, or a fully qualified class name + * + * @param string|array $key The configuration name to write. Or + * an array of multiple transports to set. + * @param array|\Cake\Mailer\AbstractTransport|null $config Either an array of configuration + * data, or a transport instance. Null when using key as array. + * @return void + * @throws \BadMethodCallException When modifying an existing configuration. + */ + public static function setConfigTransport($key, $config = null) + { + if (is_array($key)) { + foreach ($key as $name => $settings) { + static::setConfigTransport($name, $settings); + } + + return; + } + + if (isset(static::$_transportConfig[$key])) { + throw new BadMethodCallException(sprintf('Cannot modify an existing config "%s"', $key)); + } + + if (is_object($config)) { + $config = ['className' => $config]; + } + + if (isset($config['url'])) { + $parsed = static::parseDsn($config['url']); + unset($config['url']); + $config = $parsed + $config; + } + + static::$_transportConfig[$key] = $config; + } + + /** + * Gets current transport configuration. + * + * @param string $key The configuration name to read. + * @return array|null Transport config. + */ + public static function getConfigTransport($key) + { + return isset(static::$_transportConfig[$key]) ? static::$_transportConfig[$key] : null; + } + + /** + * Add or read transport configuration. + * + * Use this method to define transports to use in delivery profiles. + * Once defined you cannot edit the configurations, and must use + * Email::dropTransport() to flush the configuration first. + * + * When using an array of configuration data a new transport + * will be constructed for each message sent. When using a Closure, the + * closure will be evaluated for each message. + * + * The `className` is used to define the class to use for a transport. + * It can either be a short name, or a fully qualified classname + * + * @deprecated 3.4.0 Use setConfigTransport()/getConfigTransport() instead. + * @param string|array $key The configuration name to read/write. Or + * an array of multiple transports to set. + * @param array|\Cake\Mailer\AbstractTransport|null $config Either an array of configuration + * data, or a transport instance. + * @return array|null Either null when setting or an array of data when reading. + * @throws \BadMethodCallException When modifying an existing configuration. + */ + public static function configTransport($key, $config = null) + { + deprecationWarning('Email::configTransport() is deprecated. Use Email::setConfigTransport() or Email::getConfigTransport() instead.'); + + if ($config === null && is_string($key)) { + return static::getConfigTransport($key); + } + if ($config === null && is_array($key)) { + static::setConfigTransport($key); + + return null; + } + + static::setConfigTransport($key, $config); + } + + /** + * Returns an array containing the named transport configurations + * + * @return array Array of configurations. + */ + public static function configuredTransport() + { + return array_keys(static::$_transportConfig); + } + + /** + * Delete transport configuration. + * + * @param string $key The transport name to remove. + * @return void + */ + public static function dropTransport($key) + { + unset(static::$_transportConfig[$key]); + } + + /** + * Sets the configuration profile to use for this instance. + * + * @param string|array $config String with configuration name, or + * an array with config. + * @return $this + */ + public function setProfile($config) + { + if (!is_array($config)) { + $config = (string)$config; + } + $this->_applyConfig($config); + + return $this; + } + + /** + * Gets the configuration profile to use for this instance. + * + * @return string|array + */ + public function getProfile() + { + return $this->_profile; + } + + /** + * Get/Set the configuration profile to use for this instance. + * + * @deprecated 3.4.0 Use setProfile()/getProfile() instead. + * @param null|string|array $config String with configuration name, or + * an array with config or null to return current config. + * @return string|array|$this + */ + public function profile($config = null) + { + deprecationWarning('Email::profile() is deprecated. Use Email::setProfile() or Email::getProfile() instead.'); + + if ($config === null) { + return $this->getProfile(); + } + + return $this->setProfile($config); + } + + /** + * Send an email using the specified content, template and layout + * + * @param string|array|null $content String with message or array with messages + * @return array + * @throws \BadMethodCallException + */ + public function send($content = null) + { + if (empty($this->_from)) { + throw new BadMethodCallException('From is not specified.'); + } + if (empty($this->_to) && empty($this->_cc) && empty($this->_bcc)) { + throw new BadMethodCallException('You need specify one destination on to, cc or bcc.'); + } + + if (is_array($content)) { + $content = implode("\n", $content) . "\n"; + } + + $this->_message = $this->_render($this->_wrap($content)); + + $transport = $this->getTransport(); + if (!$transport) { + $msg = 'Cannot send email, transport was not defined. Did you call transport() or define ' . + ' a transport in the set profile?'; + throw new BadMethodCallException($msg); + } + $contents = $transport->send($this); + $this->_logDelivery($contents); + + return $contents; + } + + /** + * Log the email message delivery. + * + * @param array $contents The content with 'headers' and 'message' keys. + * @return void + */ + protected function _logDelivery($contents) + { + if (empty($this->_profile['log'])) { + return; + } + $config = [ + 'level' => 'debug', + 'scope' => 'email' + ]; + if ($this->_profile['log'] !== true) { + if (!is_array($this->_profile['log'])) { + $this->_profile['log'] = ['level' => $this->_profile['log']]; + } + $config = $this->_profile['log'] + $config; + } + Log::write( + $config['level'], + PHP_EOL . $this->flatten($contents['headers']) . PHP_EOL . PHP_EOL . $this->flatten($contents['message']), + $config['scope'] + ); + } + + /** + * Converts given value to string + * + * @param string|array $value The value to convert + * @return string + */ + protected function flatten($value) + { + return is_array($value) ? implode(';', $value) : (string)$value; + } + + /** + * Static method to fast create an instance of \Cake\Mailer\Email + * + * @param string|array|null $to Address to send (see Cake\Mailer\Email::to()). If null, will try to use 'to' from transport config + * @param string|null $subject String of subject or null to use 'subject' from transport config + * @param string|array|null $message String with message or array with variables to be used in render + * @param string|array $config String to use Email delivery profile from app.php or array with configs + * @param bool $send Send the email or just return the instance pre-configured + * @return static Instance of Cake\Mailer\Email + * @throws \InvalidArgumentException + */ + public static function deliver($to = null, $subject = null, $message = null, $config = 'default', $send = true) + { + $class = __CLASS__; + + if (is_array($config) && !isset($config['transport'])) { + $config['transport'] = 'default'; + } + /* @var \Cake\Mailer\Email $instance */ + $instance = new $class($config); + if ($to !== null) { + $instance->setTo($to); + } + if ($subject !== null) { + $instance->setSubject($subject); + } + if (is_array($message)) { + $instance->setViewVars($message); + $message = null; + } elseif ($message === null && array_key_exists('message', $config = $instance->getProfile())) { + $message = $config['message']; + } + + if ($send === true) { + $instance->send($message); + } + + return $instance; + } + + /** + * Apply the config to an instance + * + * @param string|array $config Configuration options. + * @return void + * @throws \InvalidArgumentException When using a configuration that doesn't exist. + */ + protected function _applyConfig($config) + { + if (is_string($config)) { + $name = $config; + $config = static::getConfig($name); + if (empty($config)) { + throw new InvalidArgumentException(sprintf('Unknown email configuration "%s".', $name)); + } + unset($name); + } + + $this->_profile = array_merge($this->_profile, $config); + + $simpleMethods = [ + 'from', 'sender', 'to', 'replyTo', 'readReceipt', 'returnPath', + 'cc', 'bcc', 'messageId', 'domain', 'subject', 'attachments', + 'transport', 'emailFormat', 'emailPattern', 'charset', 'headerCharset' + ]; + foreach ($simpleMethods as $method) { + if (isset($config[$method])) { + $this->{'set' . ucfirst($method)}($config[$method]); + } + } + + if (empty($this->headerCharset)) { + $this->headerCharset = $this->charset; + } + if (isset($config['headers'])) { + $this->setHeaders($config['headers']); + } + + $viewBuilderMethods = [ + 'template', 'layout', 'theme' + ]; + foreach ($viewBuilderMethods as $method) { + if (array_key_exists($method, $config)) { + $this->viewBuilder()->{'set' . ucfirst($method)}($config[$method]); + } + } + + if (array_key_exists('helpers', $config)) { + $this->viewBuilder()->setHelpers($config['helpers'], false); + } + if (array_key_exists('viewRender', $config)) { + $this->viewBuilder()->setClassName($config['viewRender']); + } + if (array_key_exists('viewVars', $config)) { + $this->set($config['viewVars']); + } + } + + /** + * Reset all the internal variables to be able to send out a new email. + * + * @return $this + */ + public function reset() + { + $this->_to = []; + $this->_from = []; + $this->_sender = []; + $this->_replyTo = []; + $this->_readReceipt = []; + $this->_returnPath = []; + $this->_cc = []; + $this->_bcc = []; + $this->_messageId = true; + $this->_subject = ''; + $this->_headers = []; + $this->_textMessage = ''; + $this->_htmlMessage = ''; + $this->_message = []; + $this->_emailFormat = 'text'; + $this->_transport = null; + $this->_priority = null; + $this->charset = 'utf-8'; + $this->headerCharset = null; + $this->transferEncoding = null; + $this->_attachments = []; + $this->_profile = []; + $this->_emailPattern = self::EMAIL_PATTERN; + + $this->viewBuilder()->setLayout('default'); + $this->viewBuilder()->setTemplate(''); + $this->viewBuilder()->setClassName('Cake\View\View'); + $this->viewVars = []; + $this->viewBuilder()->setTheme(false); + $this->viewBuilder()->setHelpers(['Html'], false); + + return $this; + } + + /** + * Encode the specified string using the current charset + * + * @param string $text String to encode + * @return string Encoded string + */ + protected function _encode($text) + { + $restore = mb_internal_encoding(); + mb_internal_encoding($this->_appCharset); + if (empty($this->headerCharset)) { + $this->headerCharset = $this->charset; + } + $return = mb_encode_mimeheader($text, $this->headerCharset, 'B'); + mb_internal_encoding($restore); + + return $return; + } + + /** + * Decode the specified string + * + * @param string $text String to decode + * @return string Decoded string + */ + protected function _decode($text) + { + $restore = mb_internal_encoding(); + mb_internal_encoding($this->_appCharset); + $return = mb_decode_mimeheader($text); + mb_internal_encoding($restore); + + return $return; + } + + /** + * Translates a string for one charset to another if the App.encoding value + * differs and the mb_convert_encoding function exists + * + * @param string $text The text to be converted + * @param string $charset the target encoding + * @return string + */ + protected function _encodeString($text, $charset) + { + if ($this->_appCharset === $charset) { + return $text; + } + + return mb_convert_encoding($text, $charset, $this->_appCharset); + } + + /** + * Wrap the message to follow the RFC 2822 - 2.1.1 + * + * @param string $message Message to wrap + * @param int $wrapLength The line length + * @return array Wrapped message + */ + protected function _wrap($message, $wrapLength = Email::LINE_LENGTH_MUST) + { + if (strlen($message) === 0) { + return ['']; + } + $message = str_replace(["\r\n", "\r"], "\n", $message); + $lines = explode("\n", $message); + $formatted = []; + $cut = ($wrapLength == Email::LINE_LENGTH_MUST); + + foreach ($lines as $line) { + if (empty($line) && $line !== '0') { + $formatted[] = ''; + continue; + } + if (strlen($line) < $wrapLength) { + $formatted[] = $line; + continue; + } + if (!preg_match('/<[a-z]+.*>/i', $line)) { + $formatted = array_merge( + $formatted, + explode("\n", Text::wordWrap($line, $wrapLength, "\n", $cut)) + ); + continue; + } + + $tagOpen = false; + $tmpLine = $tag = ''; + $tmpLineLength = 0; + for ($i = 0, $count = strlen($line); $i < $count; $i++) { + $char = $line[$i]; + if ($tagOpen) { + $tag .= $char; + if ($char === '>') { + $tagLength = strlen($tag); + if ($tagLength + $tmpLineLength < $wrapLength) { + $tmpLine .= $tag; + $tmpLineLength += $tagLength; + } else { + if ($tmpLineLength > 0) { + $formatted = array_merge( + $formatted, + explode("\n", Text::wordWrap(trim($tmpLine), $wrapLength, "\n", $cut)) + ); + $tmpLine = ''; + $tmpLineLength = 0; + } + if ($tagLength > $wrapLength) { + $formatted[] = $tag; + } else { + $tmpLine = $tag; + $tmpLineLength = $tagLength; + } + } + $tag = ''; + $tagOpen = false; + } + continue; + } + if ($char === '<') { + $tagOpen = true; + $tag = '<'; + continue; + } + if ($char === ' ' && $tmpLineLength >= $wrapLength) { + $formatted[] = $tmpLine; + $tmpLineLength = 0; + continue; + } + $tmpLine .= $char; + $tmpLineLength++; + if ($tmpLineLength === $wrapLength) { + $nextChar = $line[$i + 1]; + if ($nextChar === ' ' || $nextChar === '<') { + $formatted[] = trim($tmpLine); + $tmpLine = ''; + $tmpLineLength = 0; + if ($nextChar === ' ') { + $i++; + } + } else { + $lastSpace = strrpos($tmpLine, ' '); + if ($lastSpace === false) { + continue; + } + $formatted[] = trim(substr($tmpLine, 0, $lastSpace)); + $tmpLine = substr($tmpLine, $lastSpace + 1); + + $tmpLineLength = strlen($tmpLine); + } + } + } + if (!empty($tmpLine)) { + $formatted[] = $tmpLine; + } + } + $formatted[] = ''; + + return $formatted; + } + + /** + * Create unique boundary identifier + * + * @return void + */ + protected function _createBoundary() + { + if ($this->_attachments || $this->_emailFormat === 'both') { + $this->_boundary = md5(Security::randomBytes(16)); + } + } + + /** + * Attach non-embedded files by adding file contents inside boundaries. + * + * @param string|null $boundary Boundary to use. If null, will default to $this->_boundary + * @return array An array of lines to add to the message + */ + protected function _attachFiles($boundary = null) + { + if ($boundary === null) { + $boundary = $this->_boundary; + } + + $msg = []; + foreach ($this->_attachments as $filename => $fileInfo) { + if (!empty($fileInfo['contentId'])) { + continue; + } + $data = isset($fileInfo['data']) ? $fileInfo['data'] : $this->_readFile($fileInfo['file']); + $hasDisposition = ( + !isset($fileInfo['contentDisposition']) || + $fileInfo['contentDisposition'] + ); + $part = new FormDataPart(false, $data, false); + + if ($hasDisposition) { + $part->disposition('attachment'); + $part->filename($filename); + } + $part->transferEncoding('base64'); + $part->type($fileInfo['mimetype']); + + $msg[] = '--' . $boundary; + $msg[] = (string)$part; + $msg[] = ''; + } + + return $msg; + } + + /** + * Read the file contents and return a base64 version of the file contents. + * + * @param string $path The absolute path to the file to read. + * @return string File contents in base64 encoding + */ + protected function _readFile($path) + { + $File = new File($path); + + return chunk_split(base64_encode($File->read())); + } + + /** + * Attach inline/embedded files to the message. + * + * @param string|null $boundary Boundary to use. If null, will default to $this->_boundary + * @return array An array of lines to add to the message + */ + protected function _attachInlineFiles($boundary = null) + { + if ($boundary === null) { + $boundary = $this->_boundary; + } + + $msg = []; + foreach ($this->_attachments as $filename => $fileInfo) { + if (empty($fileInfo['contentId'])) { + continue; + } + $data = isset($fileInfo['data']) ? $fileInfo['data'] : $this->_readFile($fileInfo['file']); + + $msg[] = '--' . $boundary; + $part = new FormDataPart(false, $data, 'inline'); + $part->type($fileInfo['mimetype']); + $part->transferEncoding('base64'); + $part->contentId($fileInfo['contentId']); + $part->filename($filename); + $msg[] = (string)$part; + $msg[] = ''; + } + + return $msg; + } + + /** + * Render the body of the email. + * + * @param array $content Content to render + * @return array Email body ready to be sent + */ + protected function _render($content) + { + $this->_textMessage = $this->_htmlMessage = ''; + + $content = implode("\n", $content); + $rendered = $this->_renderTemplates($content); + + $this->_createBoundary(); + $msg = []; + + $contentIds = array_filter((array)Hash::extract($this->_attachments, '{s}.contentId')); + $hasInlineAttachments = count($contentIds) > 0; + $hasAttachments = !empty($this->_attachments); + $hasMultipleTypes = count($rendered) > 1; + $multiPart = ($hasAttachments || $hasMultipleTypes); + + $boundary = $relBoundary = $textBoundary = $this->_boundary; + + if ($hasInlineAttachments) { + $msg[] = '--' . $boundary; + $msg[] = 'Content-Type: multipart/related; boundary="rel-' . $boundary . '"'; + $msg[] = ''; + $relBoundary = $textBoundary = 'rel-' . $boundary; + } + + if ($hasMultipleTypes && $hasAttachments) { + $msg[] = '--' . $relBoundary; + $msg[] = 'Content-Type: multipart/alternative; boundary="alt-' . $boundary . '"'; + $msg[] = ''; + $textBoundary = 'alt-' . $boundary; + } + + if (isset($rendered['text'])) { + if ($multiPart) { + $msg[] = '--' . $textBoundary; + $msg[] = 'Content-Type: text/plain; charset=' . $this->_getContentTypeCharset(); + $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); + $msg[] = ''; + } + $this->_textMessage = $rendered['text']; + $content = explode("\n", $this->_textMessage); + $msg = array_merge($msg, $content); + $msg[] = ''; + } + + if (isset($rendered['html'])) { + if ($multiPart) { + $msg[] = '--' . $textBoundary; + $msg[] = 'Content-Type: text/html; charset=' . $this->_getContentTypeCharset(); + $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); + $msg[] = ''; + } + $this->_htmlMessage = $rendered['html']; + $content = explode("\n", $this->_htmlMessage); + $msg = array_merge($msg, $content); + $msg[] = ''; + } + + if ($textBoundary !== $relBoundary) { + $msg[] = '--' . $textBoundary . '--'; + $msg[] = ''; + } + + if ($hasInlineAttachments) { + $attachments = $this->_attachInlineFiles($relBoundary); + $msg = array_merge($msg, $attachments); + $msg[] = ''; + $msg[] = '--' . $relBoundary . '--'; + $msg[] = ''; + } + + if ($hasAttachments) { + $attachments = $this->_attachFiles($boundary); + $msg = array_merge($msg, $attachments); + } + if ($hasAttachments || $hasMultipleTypes) { + $msg[] = ''; + $msg[] = '--' . $boundary . '--'; + $msg[] = ''; + } + + return $msg; + } + + /** + * Gets the text body types that are in this email message + * + * @return array Array of types. Valid types are 'text' and 'html' + */ + protected function _getTypes() + { + $types = [$this->_emailFormat]; + if ($this->_emailFormat === 'both') { + $types = ['html', 'text']; + } + + return $types; + } + + /** + * Build and set all the view properties needed to render the templated emails. + * If there is no template set, the $content will be returned in a hash + * of the text content types for the email. + * + * @param string $content The content passed in from send() in most cases. + * @return array The rendered content with html and text keys. + */ + protected function _renderTemplates($content) + { + $types = $this->_getTypes(); + $rendered = []; + $template = $this->viewBuilder()->getTemplate(); + if (empty($template)) { + foreach ($types as $type) { + $rendered[$type] = $this->_encodeString($content, $this->charset); + } + + return $rendered; + } + + $View = $this->createView(); + + list($templatePlugin) = pluginSplit($View->getTemplate()); + list($layoutPlugin) = pluginSplit($View->getLayout()); + if ($templatePlugin) { + $View->plugin = $templatePlugin; + } elseif ($layoutPlugin) { + $View->plugin = $layoutPlugin; + } + + if ($View->get('content') === null) { + $View->set('content', $content); + } + + foreach ($types as $type) { + $View->hasRendered = false; + $View->setTemplatePath('Email' . DIRECTORY_SEPARATOR . $type); + $View->setLayoutPath('Email' . DIRECTORY_SEPARATOR . $type); + + $render = $View->render(); + $render = str_replace(["\r\n", "\r"], "\n", $render); + $rendered[$type] = $this->_encodeString($render, $this->charset); + } + + foreach ($rendered as $type => $content) { + $rendered[$type] = $this->_wrap($content); + $rendered[$type] = implode("\n", $rendered[$type]); + $rendered[$type] = rtrim($rendered[$type], "\n"); + } + + return $rendered; + } + + /** + * Return the Content-Transfer Encoding value based + * on the set transferEncoding or set charset. + * + * @return string + */ + protected function _getContentTransferEncoding() + { + if ($this->transferEncoding) { + return $this->transferEncoding; + } + + $charset = strtoupper($this->charset); + if (in_array($charset, $this->_charset8bit)) { + return '8bit'; + } + + return '7bit'; + } + + /** + * Return charset value for Content-Type. + * + * Checks fallback/compatibility types which include workarounds + * for legacy japanese character sets. + * + * @return string + */ + protected function _getContentTypeCharset() + { + $charset = strtoupper($this->charset); + if (array_key_exists($charset, $this->_contentTypeCharset)) { + return strtoupper($this->_contentTypeCharset[$charset]); + } + + return strtoupper($this->charset); + } + + /** + * Serializes the email object to a value that can be natively serialized and re-used + * to clone this email instance. + * + * It has certain limitations for viewVars that are good to know: + * + * - ORM\Query executed and stored as resultset + * - SimpleXMLElements stored as associative array + * - Exceptions stored as strings + * - Resources, \Closure and \PDO are not supported. + * + * @return array Serializable array of configuration properties. + * @throws \Exception When a view var object can not be properly serialized. + */ + public function jsonSerialize() + { + $properties = [ + '_to', '_from', '_sender', '_replyTo', '_cc', '_bcc', '_subject', + '_returnPath', '_readReceipt', '_emailFormat', '_emailPattern', '_domain', + '_attachments', '_messageId', '_headers', '_appCharset', 'viewVars', 'charset', 'headerCharset' + ]; + + $array = ['viewConfig' => $this->viewBuilder()->jsonSerialize()]; + + foreach ($properties as $property) { + $array[$property] = $this->{$property}; + } + + array_walk($array['_attachments'], function (&$item, $key) { + if (!empty($item['file'])) { + $item['data'] = $this->_readFile($item['file']); + unset($item['file']); + } + }); + + array_walk_recursive($array['viewVars'], [$this, '_checkViewVars']); + + return array_filter($array, function ($i) { + return !is_array($i) && strlen($i) || !empty($i); + }); + } + + /** + * Iterates through hash to clean up and normalize. + * + * @param mixed $item Reference to the view var value. + * @param string $key View var key. + * @return void + */ + protected function _checkViewVars(&$item, $key) + { + if ($item instanceof Exception) { + $item = (string)$item; + } + + if (is_resource($item) || + $item instanceof Closure || + $item instanceof PDO + ) { + throw new RuntimeException(sprintf( + 'Failed serializing the `%s` %s in the `%s` view var', + is_resource($item) ? get_resource_type($item) : get_class($item), + is_resource($item) ? 'resource' : 'object', + $key + )); + } + } + + /** + * Configures an email instance object from serialized config. + * + * @param array $config Email configuration array. + * @return $this Configured email instance. + */ + public function createFromArray($config) + { + if (isset($config['viewConfig'])) { + $this->viewBuilder()->createFromArray($config['viewConfig']); + unset($config['viewConfig']); + } + + foreach ($config as $property => $value) { + $this->{$property} = $value; + } + + return $this; + } + + /** + * Serializes the Email object. + * + * @return string + */ + public function serialize() + { + $array = $this->jsonSerialize(); + array_walk_recursive($array, function (&$item, $key) { + if ($item instanceof SimpleXMLElement) { + $item = json_decode(json_encode((array)$item), true); + } + }); + + return serialize($array); + } + + /** + * Unserializes the Email object. + * + * @param string $data Serialized string. + * @return static Configured email instance. + */ + public function unserialize($data) + { + return $this->createFromArray(unserialize($data)); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Mailer/Exception/MissingActionException.php b/app/vendor/cakephp/cakephp/src/Mailer/Exception/MissingActionException.php new file mode 100644 index 000000000..d1a9734eb --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Mailer/Exception/MissingActionException.php @@ -0,0 +1,32 @@ +setSubject('Reset Password') + * ->setTo($user->email) + * ->set(['token' => $user->token]); + * } + * } + * ``` + * + * Is a trivial example but shows how a mailer could be declared. + * + * ## Sending Messages + * + * After you have defined some messages you will want to send them: + * + * ``` + * $mailer = new UserMailer(); + * $mailer->send('resetPassword', $user); + * ``` + * + * ## Event Listener + * + * Mailers can also subscribe to application event allowing you to + * decouple email delivery from your application code. By re-declaring the + * `implementedEvents()` method you can define event handlers that can + * convert events into email. For example, if your application had a user + * registration event: + * + * ``` + * public function implementedEvents() + * { + * return [ + * 'Model.afterSave' => 'onRegistration', + * ]; + * } + * + * public function onRegistration(Event $event, Entity $entity, ArrayObject $options) + * { + * if ($entity->isNew()) { + * $this->send('welcome', [$entity]); + * } + * } + * ``` + * + * The onRegistration method converts the application event into a mailer method. + * Our mailer could either be registered in the application bootstrap, or + * in the Table class' initialize() hook. + * + * @method \Cake\Mailer\Email setTo($email, $name = null) + * @method array getTo() + * @method \Cake\Mailer\Email to($email = null, $name = null) + * @method \Cake\Mailer\Email setFrom($email, $name = null) + * @method array getFrom() + * @method \Cake\Mailer\Email from($email = null, $name = null) + * @method \Cake\Mailer\Email setSender($email, $name = null) + * @method array getSender() + * @method \Cake\Mailer\Email sender($email = null, $name = null) + * @method \Cake\Mailer\Email setReplyTo($email, $name = null) + * @method array getReplyTo() + * @method \Cake\Mailer\Email replyTo($email = null, $name = null) + * @method \Cake\Mailer\Email setReadReceipt($email, $name = null) + * @method array getReadReceipt() + * @method \Cake\Mailer\Email readReceipt($email = null, $name = null) + * @method \Cake\Mailer\Email setReturnPath($email, $name = null) + * @method array getReturnPath() + * @method \Cake\Mailer\Email returnPath($email = null, $name = null) + * @method \Cake\Mailer\Email addTo($email, $name = null) + * @method \Cake\Mailer\Email setCc($email, $name = null) + * @method array getCc() + * @method \Cake\Mailer\Email cc($email = null, $name = null) + * @method \Cake\Mailer\Email addCc($email, $name = null) + * @method \Cake\Mailer\Email setBcc($email, $name = null) + * @method array getBcc() + * @method \Cake\Mailer\Email bcc($email = null, $name = null) + * @method \Cake\Mailer\Email addBcc($email, $name = null) + * @method \Cake\Mailer\Email setCharset($charset) + * @method string getCharset() + * @method \Cake\Mailer\Email charset($charset = null) + * @method \Cake\Mailer\Email setHeaderCharset($charset) + * @method string getHeaderCharset() + * @method \Cake\Mailer\Email headerCharset($charset = null) + * @method \Cake\Mailer\Email setSubject($subject) + * @method string getSubject() + * @method \Cake\Mailer\Email subject($subject = null) + * @method \Cake\Mailer\Email setHeaders(array $headers) + * @method \Cake\Mailer\Email addHeaders(array $headers) + * @method \Cake\Mailer\Email getHeaders(array $include = []) + * @method \Cake\Mailer\Email setTemplate($template) + * @method string getTemplate() + * @method \Cake\Mailer\Email setLayout($layout) + * @method string getLayout() + * @method \Cake\Mailer\Email template($template = false, $layout = false) + * @method \Cake\Mailer\Email setViewRenderer($viewClass) + * @method string getViewRenderer() + * @method \Cake\Mailer\Email viewRender($viewClass = null) + * @method \Cake\Mailer\Email setViewVars($viewVars) + * @method array getViewVars() + * @method \Cake\Mailer\Email viewVars($viewVars = null) + * @method \Cake\Mailer\Email setTheme($theme) + * @method string getTheme() + * @method \Cake\Mailer\Email theme($theme = null) + * @method \Cake\Mailer\Email setHelpers(array $helpers) + * @method array getHelpers() + * @method \Cake\Mailer\Email helpers($helpers = null) + * @method \Cake\Mailer\Email setEmailFormat($format) + * @method string getEmailFormat() + * @method \Cake\Mailer\Email emailFormat($format = null) + * @method \Cake\Mailer\Email setTransport($name) + * @method \Cake\Mailer\AbstractTransport getTransport() + * @method \Cake\Mailer\Email transport($name = null) + * @method \Cake\Mailer\Email setMessageId($message) + * @method bool|string getMessageId() + * @method \Cake\Mailer\Email messageId($message = null) + * @method \Cake\Mailer\Email setDomain($domain) + * @method string getDomain() + * @method \Cake\Mailer\Email domain($domain = null) + * @method \Cake\Mailer\Email setAttachments($attachments) + * @method array getAttachments() + * @method \Cake\Mailer\Email attachments($attachments = null) + * @method \Cake\Mailer\Email addAttachments($attachments) + * @method \Cake\Mailer\Email message($type = null) + * @method \Cake\Mailer\Email setProfile($config) + * @method string|array getProfile() + * @method \Cake\Mailer\Email profile($config = null) + */ +abstract class Mailer implements EventListenerInterface +{ + + use ModelAwareTrait; + + /** + * Mailer's name. + * + * @var string + */ + static public $name; + + /** + * Email instance. + * + * @var \Cake\Mailer\Email + */ + protected $_email; + + /** + * Cloned Email instance for restoring instance after email is sent by + * mailer action. + * + * @var \Cake\Mailer\Email + */ + protected $_clonedEmail; + + /** + * Constructor. + * + * @param \Cake\Mailer\Email|null $email Email instance. + */ + public function __construct(Email $email = null) + { + if ($email === null) { + $email = new Email(); + } + + $this->_email = $email; + $this->_clonedEmail = clone $email; + } + + /** + * Returns the mailer's name. + * + * @return string + */ + public function getName() + { + if (!static::$name) { + static::$name = str_replace( + 'Mailer', + '', + implode('', array_slice(explode('\\', get_class($this)), -1)) + ); + } + + return static::$name; + } + + /** + * Sets layout to use. + * + * @deprecated 3.4.0 Use setLayout() which sets the layout on the email class instead. + * @param string $layout Name of the layout to use. + * @return $this + */ + public function layout($layout) + { + deprecationWarning('Mailer::layout() is deprecated. Use setLayout() which sets the layout on the email class instead.'); + + $this->_email->viewBuilder()->setLayout($layout); + + return $this; + } + + /** + * Get Email instance's view builder. + * + * @return \Cake\View\ViewBuilder + */ + public function viewBuilder() + { + return $this->_email->viewBuilder(); + } + + /** + * Magic method to forward method class to Email instance. + * + * @param string $method Method name. + * @param array $args Method arguments + * @return $this|mixed + */ + public function __call($method, $args) + { + $result = $this->_email->$method(...$args); + if (strpos($method, 'get') === 0) { + return $result; + } + + return $this; + } + + /** + * Sets email view vars. + * + * @param string|array $key Variable name or hash of view variables. + * @param mixed $value View variable value. + * @return $this + */ + public function set($key, $value = null) + { + $this->_email->setViewVars(is_string($key) ? [$key => $value] : $key); + + return $this; + } + + /** + * Sends email. + * + * @param string $action The name of the mailer action to trigger. + * @param array $args Arguments to pass to the triggered mailer action. + * @param array $headers Headers to set. + * @return array + * @throws \Cake\Mailer\Exception\MissingActionException + * @throws \BadMethodCallException + */ + public function send($action, $args = [], $headers = []) + { + try { + if (!method_exists($this, $action)) { + throw new MissingActionException([ + 'mailer' => $this->getName() . 'Mailer', + 'action' => $action, + ]); + } + + $this->_email->setHeaders($headers); + if (!$this->_email->viewBuilder()->getTemplate()) { + $this->_email->viewBuilder()->setTemplate($action); + } + + $this->$action(...$args); + + $result = $this->_email->send(); + } finally { + $this->reset(); + } + + return $result; + } + + /** + * Reset email instance. + * + * @return $this + */ + protected function reset() + { + $this->_email = clone $this->_clonedEmail; + + return $this; + } + + /** + * Implemented events. + * + * @return array + */ + public function implementedEvents() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Mailer/MailerAwareTrait.php b/app/vendor/cakephp/cakephp/src/Mailer/MailerAwareTrait.php new file mode 100644 index 000000000..8b98f50da --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Mailer/MailerAwareTrait.php @@ -0,0 +1,52 @@ +getHeaders(['from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc', 'subject']); + $headers = $this->_headersToString($headers); + $message = implode("\r\n", (array)$email->message()); + + return ['headers' => $headers, 'message' => $message]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Mailer/Transport/MailTransport.php b/app/vendor/cakephp/cakephp/src/Mailer/Transport/MailTransport.php new file mode 100644 index 000000000..885eff625 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Mailer/Transport/MailTransport.php @@ -0,0 +1,83 @@ +_config['eol'])) { + $eol = $this->_config['eol']; + } + $headers = $email->getHeaders(['from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc', 'bcc']); + $to = $headers['To']; + unset($headers['To']); + foreach ($headers as $key => $header) { + $headers[$key] = str_replace(["\r", "\n"], '', $header); + } + $headers = $this->_headersToString($headers, $eol); + $subject = str_replace(["\r", "\n"], '', $email->getSubject()); + $to = str_replace(["\r", "\n"], '', $to); + + $message = implode($eol, $email->message()); + + $params = isset($this->_config['additionalParameters']) ? $this->_config['additionalParameters'] : null; + $this->_mail($to, $subject, $message, $headers, $params); + + $headers .= $eol . 'To: ' . $to; + $headers .= $eol . 'Subject: ' . $subject; + + return ['headers' => $headers, 'message' => $message]; + } + + /** + * Wraps internal function mail() and throws exception instead of errors if anything goes wrong + * + * @param string $to email's recipient + * @param string $subject email's subject + * @param string $message email's body + * @param string $headers email's custom headers + * @param string|null $params additional params for sending email + * @throws \Cake\Network\Exception\SocketException if mail could not be sent + * @return void + */ + protected function _mail($to, $subject, $message, $headers, $params = null) + { + //@codingStandardsIgnoreStart + if (!@mail($to, $subject, $message, $headers, $params)) { + $error = error_get_last(); + $msg = 'Could not send email: ' . (isset($error['message']) ? $error['message'] : 'unknown'); + throw new SocketException($msg); + } + //@codingStandardsIgnoreEnd + } +} diff --git a/app/vendor/cakephp/cakephp/src/Mailer/Transport/SmtpTransport.php b/app/vendor/cakephp/cakephp/src/Mailer/Transport/SmtpTransport.php new file mode 100644 index 000000000..ed1795530 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Mailer/Transport/SmtpTransport.php @@ -0,0 +1,460 @@ + 'localhost', + 'port' => 25, + 'timeout' => 30, + 'username' => null, + 'password' => null, + 'client' => null, + 'tls' => false, + 'keepAlive' => false + ]; + + /** + * Socket to SMTP server + * + * @var \Cake\Network\Socket + */ + protected $_socket; + + /** + * Content of email to return + * + * @var array + */ + protected $_content = []; + + /** + * The response of the last sent SMTP command. + * + * @var array + */ + protected $_lastResponse = []; + + /** + * Destructor + * + * Tries to disconnect to ensure that the connection is being + * terminated properly before the socket gets closed. + */ + public function __destruct() + { + try { + $this->disconnect(); + } catch (Exception $e) { + // avoid fatal error on script termination + } + } + + /** + * Connect to the SMTP server. + * + * This method tries to connect only in case there is no open + * connection available already. + * + * @return void + */ + public function connect() + { + if (!$this->connected()) { + $this->_connect(); + $this->_auth(); + } + } + + /** + * Check whether an open connection to the SMTP server is available. + * + * @return bool + */ + public function connected() + { + return $this->_socket !== null && $this->_socket->connected; + } + + /** + * Disconnect from the SMTP server. + * + * This method tries to disconnect only in case there is an open + * connection available. + * + * @return void + */ + public function disconnect() + { + if ($this->connected()) { + $this->_disconnect(); + } + } + + /** + * Returns the response of the last sent SMTP command. + * + * A response consists of one or more lines containing a response + * code and an optional response message text: + * ``` + * [ + * [ + * 'code' => '250', + * 'message' => 'mail.example.com' + * ], + * [ + * 'code' => '250', + * 'message' => 'PIPELINING' + * ], + * [ + * 'code' => '250', + * 'message' => '8BITMIME' + * ], + * // etc... + * ] + * ``` + * + * @return array + */ + public function getLastResponse() + { + return $this->_lastResponse; + } + + /** + * Send mail + * + * @param \Cake\Mailer\Email $email Email instance + * @return array + * @throws \Cake\Network\Exception\SocketException + */ + public function send(Email $email) + { + if (!$this->connected()) { + $this->_connect(); + $this->_auth(); + } else { + $this->_smtpSend('RSET'); + } + + $this->_sendRcpt($email); + $this->_sendData($email); + + if (!$this->_config['keepAlive']) { + $this->_disconnect(); + } + + return $this->_content; + } + + /** + * Parses and stores the response lines in `'code' => 'message'` format. + * + * @param array $responseLines Response lines to parse. + * @return void + */ + protected function _bufferResponseLines(array $responseLines) + { + $response = []; + foreach ($responseLines as $responseLine) { + if (preg_match('/^(\d{3})(?:[ -]+(.*))?$/', $responseLine, $match)) { + $response[] = [ + 'code' => $match[1], + 'message' => isset($match[2]) ? $match[2] : null + ]; + } + } + $this->_lastResponse = array_merge($this->_lastResponse, $response); + } + + /** + * Connect to SMTP Server + * + * @return void + * @throws \Cake\Network\Exception\SocketException + */ + protected function _connect() + { + $this->_generateSocket(); + if (!$this->_socket->connect()) { + throw new SocketException('Unable to connect to SMTP server.'); + } + $this->_smtpSend(null, '220'); + + $config = $this->_config; + + if (isset($config['client'])) { + $host = $config['client']; + } elseif ($httpHost = env('HTTP_HOST')) { + list($host) = explode(':', $httpHost); + } else { + $host = 'localhost'; + } + + try { + $this->_smtpSend("EHLO {$host}", '250'); + if ($config['tls']) { + $this->_smtpSend('STARTTLS', '220'); + $this->_socket->enableCrypto('tls'); + $this->_smtpSend("EHLO {$host}", '250'); + } + } catch (SocketException $e) { + if ($config['tls']) { + throw new SocketException('SMTP server did not accept the connection or trying to connect to non TLS SMTP server using TLS.', null, $e); + } + try { + $this->_smtpSend("HELO {$host}", '250'); + } catch (SocketException $e2) { + throw new SocketException('SMTP server did not accept the connection.', null, $e2); + } + } + } + + /** + * Send authentication + * + * @return void + * @throws \Cake\Network\Exception\SocketException + */ + protected function _auth() + { + if (isset($this->_config['username'], $this->_config['password'])) { + $replyCode = (string)$this->_smtpSend('AUTH LOGIN', '334|500|502|504'); + if ($replyCode === '334') { + try { + $this->_smtpSend(base64_encode($this->_config['username']), '334'); + } catch (SocketException $e) { + throw new SocketException('SMTP server did not accept the username.', null, $e); + } + try { + $this->_smtpSend(base64_encode($this->_config['password']), '235'); + } catch (SocketException $e) { + throw new SocketException('SMTP server did not accept the password.', null, $e); + } + } elseif ($replyCode === '504') { + throw new SocketException('SMTP authentication method not allowed, check if SMTP server requires TLS.'); + } else { + throw new SocketException('AUTH command not recognized or not implemented, SMTP server may not require authentication.'); + } + } + } + + /** + * Prepares the `MAIL FROM` SMTP command. + * + * @param string $email The email address to send with the command. + * @return string + */ + protected function _prepareFromCmd($email) + { + return 'MAIL FROM:<' . $email . '>'; + } + + /** + * Prepares the `RCPT TO` SMTP command. + * + * @param string $email The email address to send with the command. + * @return string + */ + protected function _prepareRcptCmd($email) + { + return 'RCPT TO:<' . $email . '>'; + } + + /** + * Prepares the `from` email address. + * + * @param \Cake\Mailer\Email $email Email instance + * @return array + */ + protected function _prepareFromAddress($email) + { + $from = $email->getReturnPath(); + if (empty($from)) { + $from = $email->getFrom(); + } + + return $from; + } + + /** + * Prepares the recipient email addresses. + * + * @param \Cake\Mailer\Email $email Email instance + * @return array + */ + protected function _prepareRecipientAddresses($email) + { + $to = $email->getTo(); + $cc = $email->getCc(); + $bcc = $email->getBcc(); + + return array_merge(array_keys($to), array_keys($cc), array_keys($bcc)); + } + + /** + * Prepares the message headers. + * + * @param \Cake\Mailer\Email $email Email instance + * @return array + */ + protected function _prepareMessageHeaders($email) + { + return $email->getHeaders(['from', 'sender', 'replyTo', 'readReceipt', 'to', 'cc', 'subject', 'returnPath']); + } + + /** + * Prepares the message body. + * + * @param \Cake\Mailer\Email $email Email instance + * @return string + */ + protected function _prepareMessage($email) + { + $lines = $email->message(); + $messages = []; + foreach ($lines as $line) { + if (!empty($line) && ($line[0] === '.')) { + $messages[] = '.' . $line; + } else { + $messages[] = $line; + } + } + + return implode("\r\n", $messages); + } + + /** + * Send emails + * + * @return void + * @param \Cake\Mailer\Email $email Cake Email + * @throws \Cake\Network\Exception\SocketException + */ + protected function _sendRcpt($email) + { + $from = $this->_prepareFromAddress($email); + $this->_smtpSend($this->_prepareFromCmd(key($from))); + + $emails = $this->_prepareRecipientAddresses($email); + foreach ($emails as $mail) { + $this->_smtpSend($this->_prepareRcptCmd($mail)); + } + } + + /** + * Send Data + * + * @param \Cake\Mailer\Email $email Email instance + * @return void + * @throws \Cake\Network\Exception\SocketException + */ + protected function _sendData($email) + { + $this->_smtpSend('DATA', '354'); + + $headers = $this->_headersToString($this->_prepareMessageHeaders($email)); + $message = $this->_prepareMessage($email); + + $this->_smtpSend($headers . "\r\n\r\n" . $message . "\r\n\r\n\r\n."); + $this->_content = ['headers' => $headers, 'message' => $message]; + } + + /** + * Disconnect + * + * @return void + * @throws \Cake\Network\Exception\SocketException + */ + protected function _disconnect() + { + $this->_smtpSend('QUIT', false); + $this->_socket->disconnect(); + } + + /** + * Helper method to generate socket + * + * @return void + * @throws \Cake\Network\Exception\SocketException + */ + protected function _generateSocket() + { + $this->_socket = new Socket($this->_config); + } + + /** + * Protected method for sending data to SMTP connection + * + * @param string|null $data Data to be sent to SMTP server + * @param string|bool $checkCode Code to check for in server response, false to skip + * @return string|null The matched code, or null if nothing matched + * @throws \Cake\Network\Exception\SocketException + */ + protected function _smtpSend($data, $checkCode = '250') + { + $this->_lastResponse = []; + + if ($data !== null) { + $this->_socket->write($data . "\r\n"); + } + + $timeout = $this->_config['timeout']; + + while ($checkCode !== false) { + $response = ''; + $startTime = time(); + while (substr($response, -2) !== "\r\n" && ((time() - $startTime) < $timeout)) { + $bytes = $this->_socket->read(); + if ($bytes === false || $bytes === null) { + break; + } + $response .= $bytes; + } + if (substr($response, -2) !== "\r\n") { + throw new SocketException('SMTP timeout.'); + } + $responseLines = explode("\r\n", rtrim($response, "\r\n")); + $response = end($responseLines); + + $this->_bufferResponseLines($responseLines); + + if (preg_match('/^(' . $checkCode . ')(.)/', $response, $code)) { + if ($code[2] === '-') { + continue; + } + + return $code[1]; + } + throw new SocketException(sprintf('SMTP Error: %s', $response)); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Network/CorsBuilder.php b/app/vendor/cakephp/cakephp/src/Network/CorsBuilder.php new file mode 100644 index 000000000..155ac807f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Network/CorsBuilder.php @@ -0,0 +1,4 @@ + false, + 'host' => 'localhost', + 'protocol' => 'tcp', + 'port' => 80, + 'timeout' => 30 + ]; + + /** + * Reference to socket connection resource + * + * @var resource|null + */ + public $connection; + + /** + * This boolean contains the current state of the Socket class + * + * @var bool + */ + public $connected = false; + + /** + * This variable contains an array with the last error number (num) and string (str) + * + * @var array + */ + public $lastError = []; + + /** + * True if the socket stream is encrypted after a Cake\Network\Socket::enableCrypto() call + * + * @var bool + */ + public $encrypted = false; + + /** + * Contains all the encryption methods available + * + * SSLv2 and SSLv3 are deprecated, and should not be used as they + * have several published vulnerablilities. + * + * @var array + */ + protected $_encryptMethods = [ + // @codingStandardsIgnoreStart + // @deprecated Will be removed in 4.0.0 + 'sslv2_client' => STREAM_CRYPTO_METHOD_SSLv2_CLIENT, + // @deprecated Will be removed in 4.0.0 + 'sslv3_client' => STREAM_CRYPTO_METHOD_SSLv3_CLIENT, + 'sslv23_client' => STREAM_CRYPTO_METHOD_SSLv23_CLIENT, + 'tls_client' => STREAM_CRYPTO_METHOD_TLS_CLIENT, + 'tlsv10_client' => STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT, + 'tlsv11_client' => STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT, + 'tlsv12_client' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + // @deprecated Will be removed in 4.0.0 + 'sslv2_server' => STREAM_CRYPTO_METHOD_SSLv2_SERVER, + // @deprecated Will be removed in 4.0.0 + 'sslv3_server' => STREAM_CRYPTO_METHOD_SSLv3_SERVER, + 'sslv23_server' => STREAM_CRYPTO_METHOD_SSLv23_SERVER, + 'tls_server' => STREAM_CRYPTO_METHOD_TLS_SERVER, + 'tlsv10_server' => STREAM_CRYPTO_METHOD_TLSv1_0_SERVER, + 'tlsv11_server' => STREAM_CRYPTO_METHOD_TLSv1_1_SERVER, + 'tlsv12_server' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER + // @codingStandardsIgnoreEnd + ]; + + /** + * Used to capture connection warnings which can happen when there are + * SSL errors for example. + * + * @var array + */ + protected $_connectionErrors = []; + + /** + * Constructor. + * + * @param array $config Socket configuration, which will be merged with the base configuration + * @see \Cake\Network\Socket::$_baseConfig + */ + public function __construct(array $config = []) + { + $this->setConfig($config); + } + + /** + * Connect the socket to the given host and port. + * + * @return bool Success + * @throws \Cake\Network\Exception\SocketException + */ + public function connect() + { + if ($this->connection) { + $this->disconnect(); + } + + $hasProtocol = strpos($this->_config['host'], '://') !== false; + if ($hasProtocol) { + list($this->_config['protocol'], $this->_config['host']) = explode('://', $this->_config['host']); + } + $scheme = null; + if (!empty($this->_config['protocol'])) { + $scheme = $this->_config['protocol'] . '://'; + } + + $this->_setSslContext($this->_config['host']); + if (!empty($this->_config['context'])) { + $context = stream_context_create($this->_config['context']); + } else { + $context = stream_context_create(); + } + + $connectAs = STREAM_CLIENT_CONNECT; + if ($this->_config['persistent']) { + $connectAs |= STREAM_CLIENT_PERSISTENT; + } + + set_error_handler([$this, '_connectionErrorHandler']); + $this->connection = stream_socket_client( + $scheme . $this->_config['host'] . ':' . $this->_config['port'], + $errNum, + $errStr, + $this->_config['timeout'], + $connectAs, + $context + ); + restore_error_handler(); + + if (!empty($errNum) || !empty($errStr)) { + $this->setLastError($errNum, $errStr); + throw new SocketException($errStr, $errNum); + } + + if (!$this->connection && $this->_connectionErrors) { + $message = implode("\n", $this->_connectionErrors); + throw new SocketException($message, E_WARNING); + } + + $this->connected = is_resource($this->connection); + if ($this->connected) { + stream_set_timeout($this->connection, $this->_config['timeout']); + } + + return $this->connected; + } + + /** + * Configure the SSL context options. + * + * @param string $host The host name being connected to. + * @return void + */ + protected function _setSslContext($host) + { + foreach ($this->_config as $key => $value) { + if (substr($key, 0, 4) !== 'ssl_') { + continue; + } + $contextKey = substr($key, 4); + if (empty($this->_config['context']['ssl'][$contextKey])) { + $this->_config['context']['ssl'][$contextKey] = $value; + } + unset($this->_config[$key]); + } + if (!isset($this->_config['context']['ssl']['SNI_enabled'])) { + $this->_config['context']['ssl']['SNI_enabled'] = true; + } + if (empty($this->_config['context']['ssl']['peer_name'])) { + $this->_config['context']['ssl']['peer_name'] = $host; + } + if (empty($this->_config['context']['ssl']['cafile'])) { + $dir = dirname(dirname(__DIR__)); + $this->_config['context']['ssl']['cafile'] = $dir . DIRECTORY_SEPARATOR . + 'config' . DIRECTORY_SEPARATOR . 'cacert.pem'; + } + if (!empty($this->_config['context']['ssl']['verify_host'])) { + $this->_config['context']['ssl']['CN_match'] = $host; + } + unset($this->_config['context']['ssl']['verify_host']); + } + + /** + * socket_stream_client() does not populate errNum, or $errStr when there are + * connection errors, as in the case of SSL verification failure. + * + * Instead we need to handle those errors manually. + * + * @param int $code Code number. + * @param string $message Message. + * @return void + */ + protected function _connectionErrorHandler($code, $message) + { + $this->_connectionErrors[] = $message; + } + + /** + * Get the connection context. + * + * @return null|array Null when there is no connection, an array when there is. + */ + public function context() + { + if (!$this->connection) { + return null; + } + + return stream_context_get_options($this->connection); + } + + /** + * Get the host name of the current connection. + * + * @return string Host name + */ + public function host() + { + if (Validation::ip($this->_config['host'])) { + return gethostbyaddr($this->_config['host']); + } + + return gethostbyaddr($this->address()); + } + + /** + * Get the IP address of the current connection. + * + * @return string IP address + */ + public function address() + { + if (Validation::ip($this->_config['host'])) { + return $this->_config['host']; + } + + return gethostbyname($this->_config['host']); + } + + /** + * Get all IP addresses associated with the current connection. + * + * @return array IP addresses + */ + public function addresses() + { + if (Validation::ip($this->_config['host'])) { + return [$this->_config['host']]; + } + + return gethostbynamel($this->_config['host']); + } + + /** + * Get the last error as a string. + * + * @return string|null Last error + */ + public function lastError() + { + if (!empty($this->lastError)) { + return $this->lastError['num'] . ': ' . $this->lastError['str']; + } + + return null; + } + + /** + * Set the last error. + * + * @param int $errNum Error code + * @param string $errStr Error string + * @return void + */ + public function setLastError($errNum, $errStr) + { + $this->lastError = ['num' => $errNum, 'str' => $errStr]; + } + + /** + * Write data to the socket. + * + * The bool false return value is deprecated and will be int 0 in the next major. + * Please code respectively to be future proof. + * + * @param string $data The data to write to the socket. + * @return int|false Bytes written. + */ + public function write($data) + { + if (!$this->connected && !$this->connect()) { + return false; + } + $totalBytes = strlen($data); + $written = 0; + while ($written < $totalBytes) { + $rv = fwrite($this->connection, substr($data, $written)); + if ($rv === false || $rv === 0) { + return $written; + } + $written += $rv; + } + + return $written; + } + + /** + * Read data from the socket. Returns false if no data is available or no connection could be + * established. + * + * The bool false return value is deprecated and will be null in the next major. + * Please code respectively to be future proof. + * + * @param int $length Optional buffer length to read; defaults to 1024 + * @return mixed Socket data + */ + public function read($length = 1024) + { + if (!$this->connected && !$this->connect()) { + return false; + } + + if (!feof($this->connection)) { + $buffer = fread($this->connection, $length); + $info = stream_get_meta_data($this->connection); + if ($info['timed_out']) { + $this->setLastError(E_WARNING, 'Connection timed out'); + + return false; + } + + return $buffer; + } + + return false; + } + + /** + * Disconnect the socket from the current connection. + * + * @return bool Success + */ + public function disconnect() + { + if (!is_resource($this->connection)) { + $this->connected = false; + + return true; + } + $this->connected = !fclose($this->connection); + + if (!$this->connected) { + $this->connection = null; + } + + return !$this->connected; + } + + /** + * Destructor, used to disconnect from current connection. + */ + public function __destruct() + { + $this->disconnect(); + } + + /** + * Resets the state of this Socket instance to it's initial state (before Object::__construct got executed) + * + * @param array|null $state Array with key and values to reset + * @return bool True on success + */ + public function reset($state = null) + { + if (empty($state)) { + static $initalState = []; + if (empty($initalState)) { + $initalState = get_class_vars(__CLASS__); + } + $state = $initalState; + } + + foreach ($state as $property => $value) { + $this->{$property} = $value; + } + + return true; + } + + /** + * Encrypts current stream socket, using one of the defined encryption methods + * + * @param string $type can be one of 'ssl2', 'ssl3', 'ssl23' or 'tls' + * @param string $clientOrServer can be one of 'client', 'server'. Default is 'client' + * @param bool $enable enable or disable encryption. Default is true (enable) + * @return bool True on success + * @throws \InvalidArgumentException When an invalid encryption scheme is chosen. + * @throws \Cake\Network\Exception\SocketException When attempting to enable SSL/TLS fails + * @see stream_socket_enable_crypto + */ + public function enableCrypto($type, $clientOrServer = 'client', $enable = true) + { + if (!array_key_exists($type . '_' . $clientOrServer, $this->_encryptMethods)) { + throw new InvalidArgumentException('Invalid encryption scheme chosen'); + } + $method = $this->_encryptMethods[$type . '_' . $clientOrServer]; + + // Prior to PHP 5.6.7 TLS_CLIENT was any version of TLS. This was changed in 5.6.7 + // to fix backwards compatibility issues, and now only resolves to TLS1.0 + // + // See https://github.com/php/php-src/commit/10bc5fd4c4c8e1dd57bd911b086e9872a56300a0 + if (version_compare(PHP_VERSION, '5.6.7', '>=')) { + if ($method == STREAM_CRYPTO_METHOD_TLS_CLIENT) { + // @codingStandardsIgnoreStart + $method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + // @codingStandardsIgnoreEnd + } + if ($method == STREAM_CRYPTO_METHOD_TLS_SERVER) { + // @codingStandardsIgnoreStart + $method |= STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLSv1_2_SERVER; + // @codingStandardsIgnoreEnd + } + } + + try { + $enableCryptoResult = stream_socket_enable_crypto($this->connection, $enable, $method); + } catch (Exception $e) { + $this->setLastError(null, $e->getMessage()); + throw new SocketException($e->getMessage(), null, $e); + } + if ($enableCryptoResult === true) { + $this->encrypted = $enable; + + return true; + } + $errorMessage = 'Unable to perform enableCrypto operation on the current socket'; + $this->setLastError(null, $errorMessage); + throw new SocketException($errorMessage); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association.php b/app/vendor/cakephp/cakephp/src/ORM/Association.php new file mode 100644 index 000000000..eda333f54 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Association.php @@ -0,0 +1,1491 @@ +{'_' . $property} = $options[$property]; + } + } + + if (empty($this->_className) && strpos($alias, '.')) { + $this->_className = $alias; + } + + list(, $name) = pluginSplit($alias); + $this->_name = $name; + + $this->_options($options); + + if (!empty($options['strategy'])) { + $this->setStrategy($options['strategy']); + } + } + + /** + * Sets the name for this association, usually the alias + * assigned to the target associated table + * + * @param string $name Name to be assigned + * @return $this + */ + public function setName($name) + { + if ($this->_targetTable !== null) { + $alias = $this->_targetTable->getAlias(); + if ($alias !== $name) { + throw new InvalidArgumentException('Association name does not match target table alias.'); + } + } + + $this->_name = $name; + + return $this; + } + + /** + * Gets the name for this association, usually the alias + * assigned to the target associated table + * + * @return string + */ + public function getName() + { + return $this->_name; + } + + /** + * Sets the name for this association. + * + * @deprecated 3.4.0 Use setName()/getName() instead. + * @param string|null $name Name to be assigned + * @return string + */ + public function name($name = null) + { + deprecationWarning( + get_called_class() . '::name() is deprecated. ' . + 'Use setName()/getName() instead.' + ); + if ($name !== null) { + $this->setName($name); + } + + return $this->getName(); + } + + /** + * Sets whether or not cascaded deletes should also fire callbacks. + * + * @param bool $cascadeCallbacks cascade callbacks switch value + * @return $this + */ + public function setCascadeCallbacks($cascadeCallbacks) + { + $this->_cascadeCallbacks = $cascadeCallbacks; + + return $this; + } + + /** + * Gets whether or not cascaded deletes should also fire callbacks. + * + * @return bool + */ + public function getCascadeCallbacks() + { + return $this->_cascadeCallbacks; + } + + /** + * Sets whether or not cascaded deletes should also fire callbacks. If no + * arguments are passed, the current configured value is returned + * + * @deprecated 3.4.0 Use setCascadeCallbacks()/getCascadeCallbacks() instead. + * @param bool|null $cascadeCallbacks cascade callbacks switch value + * @return bool + */ + public function cascadeCallbacks($cascadeCallbacks = null) + { + deprecationWarning( + get_called_class() . '::cascadeCallbacks() is deprecated. ' . + 'Use setCascadeCallbacks()/getCascadeCallbacks() instead.' + ); + if ($cascadeCallbacks !== null) { + $this->setCascadeCallbacks($cascadeCallbacks); + } + + return $this->getCascadeCallbacks(); + } + + /** + * The class name of the target table object + * + * @return string + */ + public function className() + { + return $this->_className; + } + + /** + * Sets the table instance for the source side of the association. + * + * @param \Cake\ORM\Table $table the instance to be assigned as source side + * @return $this + */ + public function setSource(Table $table) + { + $this->_sourceTable = $table; + + return $this; + } + + /** + * Gets the table instance for the source side of the association. + * + * @return \Cake\ORM\Table + */ + public function getSource() + { + return $this->_sourceTable; + } + + /** + * Sets the table instance for the source side of the association. If no arguments + * are passed, the current configured table instance is returned + * + * @deprecated 3.4.0 Use setSource()/getSource() instead. + * @param \Cake\ORM\Table|null $table the instance to be assigned as source side + * @return \Cake\ORM\Table + */ + public function source(Table $table = null) + { + deprecationWarning( + get_called_class() . '::source() is deprecated. ' . + 'Use setSource()/getSource() instead.' + ); + if ($table === null) { + return $this->_sourceTable; + } + + return $this->_sourceTable = $table; + } + + /** + * Sets the table instance for the target side of the association. + * + * @param \Cake\ORM\Table $table the instance to be assigned as target side + * @return $this + */ + public function setTarget(Table $table) + { + $this->_targetTable = $table; + + return $this; + } + + /** + * Gets the table instance for the target side of the association. + * + * @return \Cake\ORM\Table + */ + public function getTarget() + { + if (!$this->_targetTable) { + if (strpos($this->_className, '.')) { + list($plugin) = pluginSplit($this->_className, true); + $registryAlias = $plugin . $this->_name; + } else { + $registryAlias = $this->_name; + } + + $tableLocator = $this->getTableLocator(); + + $config = []; + $exists = $tableLocator->exists($registryAlias); + if (!$exists) { + $config = ['className' => $this->_className]; + } + $this->_targetTable = $tableLocator->get($registryAlias, $config); + + if ($exists) { + $className = $this->_getClassName($registryAlias, ['className' => $this->_className]); + + if (!$this->_targetTable instanceof $className) { + $errorMessage = '%s association "%s" of type "%s" to "%s" doesn\'t match the expected class "%s". '; + $errorMessage .= 'You can\'t have an association of the same name with a different target "className" option anywhere in your app.'; + + throw new RuntimeException(sprintf( + $errorMessage, + $this->_sourceTable ? get_class($this->_sourceTable) : 'null', + $this->getName(), + $this->type(), + $this->_targetTable ? get_class($this->_targetTable) : 'null', + $className + )); + } + } + } + + return $this->_targetTable; + } + + /** + * Sets the table instance for the target side of the association. If no arguments + * are passed, the current configured table instance is returned + * + * @deprecated 3.4.0 Use setTarget()/getTarget() instead. + * @param \Cake\ORM\Table|null $table the instance to be assigned as target side + * @return \Cake\ORM\Table + */ + public function target(Table $table = null) + { + deprecationWarning( + get_called_class() . '::target() is deprecated. ' . + 'Use setTarget()/getTarget() instead.' + ); + if ($table !== null) { + $this->setTarget($table); + } + + return $this->getTarget(); + } + + /** + * Sets a list of conditions to be always included when fetching records from + * the target association. + * + * @param array|callable $conditions list of conditions to be used + * @see \Cake\Database\Query::where() for examples on the format of the array + * @return $this + */ + public function setConditions($conditions) + { + $this->_conditions = $conditions; + + return $this; + } + + /** + * Gets a list of conditions to be always included when fetching records from + * the target association. + * + * @see \Cake\Database\Query::where() for examples on the format of the array + * @return array|callable + */ + public function getConditions() + { + return $this->_conditions; + } + + /** + * Sets a list of conditions to be always included when fetching records from + * the target association. If no parameters are passed the current list is returned + * + * @deprecated 3.4.0 Use setConditions()/getConditions() instead. + * @param array|null $conditions list of conditions to be used + * @see \Cake\Database\Query::where() for examples on the format of the array + * @return array|callable + */ + public function conditions($conditions = null) + { + deprecationWarning( + get_called_class() . '::conditions() is deprecated. ' . + 'Use setConditions()/getConditions() instead.' + ); + if ($conditions !== null) { + $this->setConditions($conditions); + } + + return $this->getConditions(); + } + + /** + * Sets the name of the field representing the binding field with the target table. + * When not manually specified the primary key of the owning side table is used. + * + * @param string|array $key the table field or fields to be used to link both tables together + * @return $this + */ + public function setBindingKey($key) + { + $this->_bindingKey = $key; + + return $this; + } + + /** + * Gets the name of the field representing the binding field with the target table. + * When not manually specified the primary key of the owning side table is used. + * + * @return string|array + */ + public function getBindingKey() + { + if ($this->_bindingKey === null) { + $this->_bindingKey = $this->isOwningSide($this->getSource()) ? + $this->getSource()->getPrimaryKey() : + $this->getTarget()->getPrimaryKey(); + } + + return $this->_bindingKey; + } + + /** + * Sets the name of the field representing the binding field with the target table. + * When not manually specified the primary key of the owning side table is used. + * + * If no parameters are passed the current field is returned + * + * @deprecated 3.4.0 Use setBindingKey()/getBindingKey() instead. + * @param string|null $key the table field to be used to link both tables together + * @return string|array + */ + public function bindingKey($key = null) + { + deprecationWarning( + get_called_class() . '::bindingKey() is deprecated. ' . + 'Use setBindingKey()/getBindingKey() instead.' + ); + if ($key !== null) { + $this->setBindingKey($key); + } + + return $this->getBindingKey(); + } + + /** + * Gets the name of the field representing the foreign key to the target table. + * + * @return string|array + */ + public function getForeignKey() + { + return $this->_foreignKey; + } + + /** + * Sets the name of the field representing the foreign key to the target table. + * + * @param string|array $key the key or keys to be used to link both tables together + * @return $this + */ + public function setForeignKey($key) + { + $this->_foreignKey = $key; + + return $this; + } + + /** + * Sets the name of the field representing the foreign key to the target table. + * If no parameters are passed the current field is returned + * + * @deprecated 3.4.0 Use setForeignKey()/getForeignKey() instead. + * @param string|null $key the key to be used to link both tables together + * @return string|array + */ + public function foreignKey($key = null) + { + deprecationWarning( + get_called_class() . '::foreignKey() is deprecated. ' . + 'Use setForeignKey()/getForeignKey() instead.' + ); + if ($key !== null) { + $this->setForeignKey($key); + } + + return $this->getForeignKey(); + } + + /** + * Sets whether the records on the target table are dependent on the source table. + * + * This is primarily used to indicate that records should be removed if the owning record in + * the source table is deleted. + * + * If no parameters are passed the current setting is returned. + * + * @param bool $dependent Set the dependent mode. Use null to read the current state. + * @return $this + */ + public function setDependent($dependent) + { + $this->_dependent = $dependent; + + return $this; + } + + /** + * Sets whether the records on the target table are dependent on the source table. + * + * This is primarily used to indicate that records should be removed if the owning record in + * the source table is deleted. + * + * @return bool + */ + public function getDependent() + { + return $this->_dependent; + } + + /** + * Sets whether the records on the target table are dependent on the source table. + * + * This is primarily used to indicate that records should be removed if the owning record in + * the source table is deleted. + * + * If no parameters are passed the current setting is returned. + * + * @deprecated 3.4.0 Use setDependent()/getDependent() instead. + * @param bool|null $dependent Set the dependent mode. Use null to read the current state. + * @return bool + */ + public function dependent($dependent = null) + { + deprecationWarning( + get_called_class() . '::dependent() is deprecated. ' . + 'Use setDependent()/getDependent() instead.' + ); + if ($dependent !== null) { + $this->setDependent($dependent); + } + + return $this->getDependent(); + } + + /** + * Whether this association can be expressed directly in a query join + * + * @param array $options custom options key that could alter the return value + * @return bool + */ + public function canBeJoined(array $options = []) + { + $strategy = isset($options['strategy']) ? $options['strategy'] : $this->getStrategy(); + + return $strategy == $this::STRATEGY_JOIN; + } + + /** + * Sets the type of join to be used when adding the association to a query. + * + * @param string $type the join type to be used (e.g. INNER) + * @return $this + */ + public function setJoinType($type) + { + $this->_joinType = $type; + + return $this; + } + + /** + * Gets the type of join to be used when adding the association to a query. + * + * @return string + */ + public function getJoinType() + { + return $this->_joinType; + } + + /** + * Sets the type of join to be used when adding the association to a query. + * If no arguments are passed, the currently configured type is returned. + * + * @deprecated 3.4.0 Use setJoinType()/getJoinType() instead. + * @param string|null $type the join type to be used (e.g. INNER) + * @return string + */ + public function joinType($type = null) + { + deprecationWarning( + get_called_class() . '::joinType() is deprecated. ' . + 'Use setJoinType()/getJoinType() instead.' + ); + if ($type !== null) { + $this->setJoinType($type); + } + + return $this->getJoinType(); + } + + /** + * Sets the property name that should be filled with data from the target table + * in the source table record. + * + * @param string $name The name of the association property. Use null to read the current value. + * @return $this + */ + public function setProperty($name) + { + $this->_propertyName = $name; + + return $this; + } + + /** + * Gets the property name that should be filled with data from the target table + * in the source table record. + * + * @return string + */ + public function getProperty() + { + if (!$this->_propertyName) { + $this->_propertyName = $this->_propertyName(); + if (in_array($this->_propertyName, $this->_sourceTable->getSchema()->columns())) { + $msg = 'Association property name "%s" clashes with field of same name of table "%s".' . + ' You should explicitly specify the "propertyName" option.'; + trigger_error( + sprintf($msg, $this->_propertyName, $this->_sourceTable->getTable()), + E_USER_WARNING + ); + } + } + + return $this->_propertyName; + } + + /** + * Sets the property name that should be filled with data from the target table + * in the source table record. + * If no arguments are passed, the currently configured type is returned. + * + * @deprecated 3.4.0 Use setProperty()/getProperty() instead. + * @param string|null $name The name of the association property. Use null to read the current value. + * @return string + */ + public function property($name = null) + { + deprecationWarning( + get_called_class() . '::property() is deprecated. ' . + 'Use setProperty()/getProperty() instead.' + ); + if ($name !== null) { + $this->setProperty($name); + } + + return $this->getProperty(); + } + + /** + * Returns default property name based on association name. + * + * @return string + */ + protected function _propertyName() + { + list(, $name) = pluginSplit($this->_name); + + return Inflector::underscore($name); + } + + /** + * Sets the strategy name to be used to fetch associated records. Keep in mind + * that some association types might not implement but a default strategy, + * rendering any changes to this setting void. + * + * @param string $name The strategy type. Use null to read the current value. + * @return $this + * @throws \InvalidArgumentException When an invalid strategy is provided. + */ + public function setStrategy($name) + { + if (!in_array($name, $this->_validStrategies)) { + throw new InvalidArgumentException( + sprintf('Invalid strategy "%s" was provided', $name) + ); + } + $this->_strategy = $name; + + return $this; + } + + /** + * Gets the strategy name to be used to fetch associated records. Keep in mind + * that some association types might not implement but a default strategy, + * rendering any changes to this setting void. + * + * @return string + */ + public function getStrategy() + { + return $this->_strategy; + } + + /** + * Sets the strategy name to be used to fetch associated records. Keep in mind + * that some association types might not implement but a default strategy, + * rendering any changes to this setting void. + * If no arguments are passed, the currently configured strategy is returned. + * + * @deprecated 3.4.0 Use setStrategy()/getStrategy() instead. + * @param string|null $name The strategy type. Use null to read the current value. + * @return string + * @throws \InvalidArgumentException When an invalid strategy is provided. + */ + public function strategy($name = null) + { + deprecationWarning( + get_called_class() . '::strategy() is deprecated. ' . + 'Use setStrategy()/getStrategy() instead.' + ); + if ($name !== null) { + $this->setStrategy($name); + } + + return $this->getStrategy(); + } + + /** + * Gets the default finder to use for fetching rows from the target table. + * + * @return string + */ + public function getFinder() + { + return $this->_finder; + } + + /** + * Sets the default finder to use for fetching rows from the target table. + * + * @param string $finder the finder name to use + * @return $this + */ + public function setFinder($finder) + { + $this->_finder = $finder; + + return $this; + } + + /** + * Sets the default finder to use for fetching rows from the target table. + * If no parameters are passed, it will return the currently configured + * finder name. + * + * @deprecated 3.4.0 Use setFinder()/getFinder() instead. + * @param string|null $finder the finder name to use + * @return string + */ + public function finder($finder = null) + { + deprecationWarning( + get_called_class() . '::finder() is deprecated. ' . + 'Use setFinder()/getFinder() instead.' + ); + if ($finder !== null) { + $this->setFinder($finder); + } + + return $this->getFinder(); + } + + /** + * Override this function to initialize any concrete association class, it will + * get passed the original list of options used in the constructor + * + * @param array $options List of options used for initialization + * @return void + */ + protected function _options(array $options) + { + } + + /** + * Alters a Query object to include the associated target table data in the final + * result + * + * The options array accept the following keys: + * + * - includeFields: Whether to include target model fields in the result or not + * - foreignKey: The name of the field to use as foreign key, if false none + * will be used + * - conditions: array with a list of conditions to filter the join with, this + * will be merged with any conditions originally configured for this association + * - fields: a list of fields in the target table to include in the result + * - type: The type of join to be used (e.g. INNER) + * the records found on this association + * - aliasPath: A dot separated string representing the path of association names + * followed from the passed query main table to this association. + * - propertyPath: A dot separated string representing the path of association + * properties to be followed from the passed query main entity to this + * association + * - joinType: The SQL join type to use in the query. + * - negateMatch: Will append a condition to the passed query for excluding matches. + * with this association. + * + * @param \Cake\ORM\Query $query the query to be altered to include the target table data + * @param array $options Any extra options or overrides to be taken in account + * @return void + * @throws \RuntimeException if the query builder passed does not return a query + * object + */ + public function attachTo(Query $query, array $options = []) + { + $target = $this->getTarget(); + $joinType = empty($options['joinType']) ? $this->getJoinType() : $options['joinType']; + $table = $target->getTable(); + + $options += [ + 'includeFields' => true, + 'foreignKey' => $this->getForeignKey(), + 'conditions' => [], + 'fields' => [], + 'type' => $joinType, + 'table' => $table, + 'finder' => $this->getFinder() + ]; + + if (!empty($options['foreignKey'])) { + $joinCondition = $this->_joinCondition($options); + if ($joinCondition) { + $options['conditions'][] = $joinCondition; + } + } + + list($finder, $opts) = $this->_extractFinder($options['finder']); + $dummy = $this + ->find($finder, $opts) + ->eagerLoaded(true); + + if (!empty($options['queryBuilder'])) { + $dummy = $options['queryBuilder']($dummy); + if (!($dummy instanceof Query)) { + throw new RuntimeException(sprintf( + 'Query builder for association "%s" did not return a query', + $this->getName() + )); + } + } + + $dummy->where($options['conditions']); + $this->_dispatchBeforeFind($dummy); + + $joinOptions = ['table' => 1, 'conditions' => 1, 'type' => 1]; + $options['conditions'] = $dummy->clause('where'); + $query->join([$this->_name => array_intersect_key($options, $joinOptions)]); + + $this->_appendFields($query, $dummy, $options); + $this->_formatAssociationResults($query, $dummy, $options); + $this->_bindNewAssociations($query, $dummy, $options); + $this->_appendNotMatching($query, $options); + } + + /** + * Conditionally adds a condition to the passed Query that will make it find + * records where there is no match with this association. + * + * @param \Cake\Datasource\QueryInterface $query The query to modify + * @param array $options Options array containing the `negateMatch` key. + * @return void + */ + protected function _appendNotMatching($query, $options) + { + $target = $this->_targetTable; + if (!empty($options['negateMatch'])) { + $primaryKey = $query->aliasFields((array)$target->getPrimaryKey(), $this->_name); + $query->andWhere(function ($exp) use ($primaryKey) { + array_map([$exp, 'isNull'], $primaryKey); + + return $exp; + }); + } + } + + /** + * Correctly nests a result row associated values into the correct array keys inside the + * source results. + * + * @param array $row The row to transform + * @param string $nestKey The array key under which the results for this association + * should be found + * @param bool $joined Whether or not the row is a result of a direct join + * with this association + * @param string|null $targetProperty The property name in the source results where the association + * data shuld be nested in. Will use the default one if not provided. + * @return array + */ + public function transformRow($row, $nestKey, $joined, $targetProperty = null) + { + $sourceAlias = $this->getSource()->getAlias(); + $nestKey = $nestKey ?: $this->_name; + $targetProperty = $targetProperty ?: $this->getProperty(); + if (isset($row[$sourceAlias])) { + $row[$sourceAlias][$targetProperty] = $row[$nestKey]; + unset($row[$nestKey]); + } + + return $row; + } + + /** + * Returns a modified row after appending a property for this association + * with the default empty value according to whether the association was + * joined or fetched externally. + * + * @param array $row The row to set a default on. + * @param bool $joined Whether or not the row is a result of a direct join + * with this association + * @return array + */ + public function defaultRowValue($row, $joined) + { + $sourceAlias = $this->getSource()->getAlias(); + if (isset($row[$sourceAlias])) { + $row[$sourceAlias][$this->getProperty()] = null; + } + + return $row; + } + + /** + * Proxies the finding operation to the target table's find method + * and modifies the query accordingly based of this association + * configuration + * + * @param string|array|null $type the type of query to perform, if an array is passed, + * it will be interpreted as the `$options` parameter + * @param array $options The options to for the find + * @see \Cake\ORM\Table::find() + * @return \Cake\ORM\Query + */ + public function find($type = null, array $options = []) + { + $type = $type ?: $this->getFinder(); + list($type, $opts) = $this->_extractFinder($type); + + return $this->getTarget() + ->find($type, $options + $opts) + ->where($this->getConditions()); + } + + /** + * Proxies the operation to the target table's exists method after + * appending the default conditions for this association + * + * @param array|callable|\Cake\Database\ExpressionInterface $conditions The conditions to use + * for checking if any record matches. + * @see \Cake\ORM\Table::exists() + * @return bool + */ + public function exists($conditions) + { + if ($this->_conditions) { + $conditions = $this + ->find('all', ['conditions' => $conditions]) + ->clause('where'); + } + + return $this->getTarget()->exists($conditions); + } + + /** + * Proxies the update operation to the target table's updateAll method + * + * @param array $fields A hash of field => new value. + * @param mixed $conditions Conditions to be used, accepts anything Query::where() + * can take. + * @see \Cake\ORM\Table::updateAll() + * @return int Count Returns the affected rows. + */ + public function updateAll($fields, $conditions) + { + $target = $this->getTarget(); + $expression = $target->query() + ->where($this->getConditions()) + ->where($conditions) + ->clause('where'); + + return $target->updateAll($fields, $expression); + } + + /** + * Proxies the delete operation to the target table's deleteAll method + * + * @param mixed $conditions Conditions to be used, accepts anything Query::where() + * can take. + * @return int Returns the number of affected rows. + * @see \Cake\ORM\Table::deleteAll() + */ + public function deleteAll($conditions) + { + $target = $this->getTarget(); + $expression = $target->query() + ->where($this->getConditions()) + ->where($conditions) + ->clause('where'); + + return $target->deleteAll($expression); + } + + /** + * Returns true if the eager loading process will require a set of the owning table's + * binding keys in order to use them as a filter in the finder query. + * + * @param array $options The options containing the strategy to be used. + * @return bool true if a list of keys will be required + */ + public function requiresKeys(array $options = []) + { + $strategy = isset($options['strategy']) ? $options['strategy'] : $this->getStrategy(); + + return $strategy === static::STRATEGY_SELECT; + } + + /** + * Triggers beforeFind on the target table for the query this association is + * attaching to + * + * @param \Cake\ORM\Query $query the query this association is attaching itself to + * @return void + */ + protected function _dispatchBeforeFind($query) + { + $query->triggerBeforeFind(); + } + + /** + * Helper function used to conditionally append fields to the select clause of + * a query from the fields found in another query object. + * + * @param \Cake\ORM\Query $query the query that will get the fields appended to + * @param \Cake\ORM\Query $surrogate the query having the fields to be copied from + * @param array $options options passed to the method `attachTo` + * @return void + */ + protected function _appendFields($query, $surrogate, $options) + { + if ($query->getEagerLoader()->isAutoFieldsEnabled() === false) { + return; + } + + $fields = $surrogate->clause('select') ?: $options['fields']; + $target = $this->_targetTable; + $autoFields = $surrogate->isAutoFieldsEnabled(); + + if (empty($fields) && !$autoFields) { + if ($options['includeFields'] && ($fields === null || $fields !== false)) { + $fields = $target->getSchema()->columns(); + } + } + + if ($autoFields === true) { + $fields = array_filter((array)$fields); + $fields = array_merge($fields, $target->getSchema()->columns()); + } + + if ($fields) { + $query->select($query->aliasFields($fields, $this->_name)); + } + $query->addDefaultTypes($target); + } + + /** + * Adds a formatter function to the passed `$query` if the `$surrogate` query + * declares any other formatter. Since the `$surrogate` query correspond to + * the associated target table, the resulting formatter will be the result of + * applying the surrogate formatters to only the property corresponding to + * such table. + * + * @param \Cake\ORM\Query $query the query that will get the formatter applied to + * @param \Cake\ORM\Query $surrogate the query having formatters for the associated + * target table. + * @param array $options options passed to the method `attachTo` + * @return void + */ + protected function _formatAssociationResults($query, $surrogate, $options) + { + $formatters = $surrogate->getResultFormatters(); + + if (!$formatters || empty($options['propertyPath'])) { + return; + } + + $property = $options['propertyPath']; + $propertyPath = explode('.', $property); + $query->formatResults(function ($results) use ($formatters, $property, $propertyPath) { + $extracted = []; + foreach ($results as $result) { + foreach ($propertyPath as $propertyPathItem) { + if (!isset($result[$propertyPathItem])) { + $result = null; + break; + } + $result = $result[$propertyPathItem]; + } + $extracted[] = $result; + } + $extracted = new Collection($extracted); + foreach ($formatters as $callable) { + $extracted = new ResultSetDecorator($callable($extracted)); + } + + /* @var \Cake\Collection\CollectionInterface $results */ + return $results->insert($property, $extracted); + }, Query::PREPEND); + } + + /** + * Applies all attachable associations to `$query` out of the containments found + * in the `$surrogate` query. + * + * Copies all contained associations from the `$surrogate` query into the + * passed `$query`. Containments are altered so that they respect the associations + * chain from which they originated. + * + * @param \Cake\ORM\Query $query the query that will get the associations attached to + * @param \Cake\ORM\Query $surrogate the query having the containments to be attached + * @param array $options options passed to the method `attachTo` + * @return void + */ + protected function _bindNewAssociations($query, $surrogate, $options) + { + $loader = $surrogate->getEagerLoader(); + $contain = $loader->getContain(); + $matching = $loader->getMatching(); + + if (!$contain && !$matching) { + return; + } + + $newContain = []; + foreach ($contain as $alias => $value) { + $newContain[$options['aliasPath'] . '.' . $alias] = $value; + } + + $eagerLoader = $query->getEagerLoader(); + if ($newContain) { + $eagerLoader->contain($newContain); + } + + foreach ($matching as $alias => $value) { + $eagerLoader->setMatching( + $options['aliasPath'] . '.' . $alias, + $value['queryBuilder'], + $value + ); + } + } + + /** + * Returns a single or multiple conditions to be appended to the generated join + * clause for getting the results on the target table. + * + * @param array $options list of options passed to attachTo method + * @return array + * @throws \RuntimeException if the number of columns in the foreignKey do not + * match the number of columns in the source table primaryKey + */ + protected function _joinCondition($options) + { + $conditions = []; + $tAlias = $this->_name; + $sAlias = $this->getSource()->getAlias(); + $foreignKey = (array)$options['foreignKey']; + $bindingKey = (array)$this->getBindingKey(); + + if (count($foreignKey) !== count($bindingKey)) { + if (empty($bindingKey)) { + $table = $this->getTarget()->getTable(); + if ($this->isOwningSide($this->getSource())) { + $table = $this->getSource()->getTable(); + } + $msg = 'The "%s" table does not define a primary key, and cannot have join conditions generated.'; + throw new RuntimeException(sprintf($msg, $table)); + } + + $msg = 'Cannot match provided foreignKey for "%s", got "(%s)" but expected foreign key for "(%s)"'; + throw new RuntimeException(sprintf( + $msg, + $this->_name, + implode(', ', $foreignKey), + implode(', ', $bindingKey) + )); + } + + foreach ($foreignKey as $k => $f) { + $field = sprintf('%s.%s', $sAlias, $bindingKey[$k]); + $value = new IdentifierExpression(sprintf('%s.%s', $tAlias, $f)); + $conditions[$field] = $value; + } + + return $conditions; + } + + /** + * Helper method to infer the requested finder and its options. + * + * Returns the inferred options from the finder $type. + * + * ### Examples: + * + * The following will call the finder 'translations' with the value of the finder as its options: + * $query->contain(['Comments' => ['finder' => ['translations']]]); + * $query->contain(['Comments' => ['finder' => ['translations' => []]]]); + * $query->contain(['Comments' => ['finder' => ['translations' => ['locales' => ['en_US']]]]]); + * + * @param string|array $finderData The finder name or an array having the name as key + * and options as value. + * @return array + */ + protected function _extractFinder($finderData) + { + $finderData = (array)$finderData; + + if (is_numeric(key($finderData))) { + return [current($finderData), []]; + } + + return [key($finderData), current($finderData)]; + } + + /** + * Gets the table class name. + * + * @param string $alias The alias name you want to get. + * @param array $options Table options array. + * @return string + */ + protected function _getClassName($alias, array $options = []) + { + if (empty($options['className'])) { + $options['className'] = Inflector::camelize($alias); + } + + $className = App::className($options['className'], 'Model/Table', 'Table') ?: 'Cake\ORM\Table'; + + return ltrim($className, '\\'); + } + + /** + * Proxies property retrieval to the target table. This is handy for getting this + * association's associations + * + * @param string $property the property name + * @return \Cake\ORM\Association + * @throws \RuntimeException if no association with such name exists + */ + public function __get($property) + { + return $this->getTarget()->{$property}; + } + + /** + * Proxies the isset call to the target table. This is handy to check if the + * target table has another association with the passed name + * + * @param string $property the property name + * @return bool true if the property exists + */ + public function __isset($property) + { + return isset($this->getTarget()->{$property}); + } + + /** + * Proxies method calls to the target table. + * + * @param string $method name of the method to be invoked + * @param array $argument List of arguments passed to the function + * @return mixed + * @throws \BadMethodCallException + */ + public function __call($method, $argument) + { + return $this->getTarget()->$method(...$argument); + } + + /** + * Get the relationship type. + * + * @return string Constant of either ONE_TO_ONE, MANY_TO_ONE, ONE_TO_MANY or MANY_TO_MANY. + */ + abstract public function type(); + + /** + * Eager loads a list of records in the target table that are related to another + * set of records in the source table. Source records can specified in two ways: + * first one is by passing a Query object setup to find on the source table and + * the other way is by explicitly passing an array of primary key values from + * the source table. + * + * The required way of passing related source records is controlled by "strategy" + * When the subquery strategy is used it will require a query on the source table. + * When using the select strategy, the list of primary keys will be used. + * + * Returns a closure that should be run for each record returned in a specific + * Query. This callable will be responsible for injecting the fields that are + * related to each specific passed row. + * + * Options array accepts the following keys: + * + * - query: Query object setup to find the source table records + * - keys: List of primary key values from the source table + * - foreignKey: The name of the field used to relate both tables + * - conditions: List of conditions to be passed to the query where() method + * - sort: The direction in which the records should be returned + * - fields: List of fields to select from the target table + * - contain: List of related tables to eager load associated to the target table + * - strategy: The name of strategy to use for finding target table records + * - nestKey: The array key under which results will be found when transforming the row + * + * @param array $options The options for eager loading. + * @return \Closure + */ + abstract public function eagerLoader(array $options); + + /** + * Handles cascading a delete from an associated model. + * + * Each implementing class should handle the cascaded delete as + * required. + * + * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascaded delete. + * @param array $options The options for the original delete. + * @return bool Success + */ + abstract public function cascadeDelete(EntityInterface $entity, array $options = []); + + /** + * Returns whether or not the passed table is the owning side for this + * association. This means that rows in the 'target' table would miss important + * or required information if the row in 'source' did not exist. + * + * @param \Cake\ORM\Table $side The potential Table with ownership + * @return bool + */ + abstract public function isOwningSide(Table $side); + + /** + * Extract the target's association data our from the passed entity and proxies + * the saving operation to the target table. + * + * @param \Cake\Datasource\EntityInterface $entity the data to be saved + * @param array $options The options for saving associated data. + * @return bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns + * the saved entity + * @see \Cake\ORM\Table::save() + */ + abstract public function saveAssociated(EntityInterface $entity, array $options = []); +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsTo.php b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsTo.php new file mode 100644 index 000000000..651f81e30 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsTo.php @@ -0,0 +1,202 @@ +_foreignKey === null) { + $this->_foreignKey = $this->_modelKey($this->getTarget()->getAlias()); + } + + return $this->_foreignKey; + } + + /** + * Handle cascading deletes. + * + * BelongsTo associations are never cleared in a cascading delete scenario. + * + * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascaded delete. + * @param array $options The options for the original delete. + * @return bool Success. + */ + public function cascadeDelete(EntityInterface $entity, array $options = []) + { + return true; + } + + /** + * Returns default property name based on association name. + * + * @return string + */ + protected function _propertyName() + { + list(, $name) = pluginSplit($this->_name); + + return Inflector::underscore(Inflector::singularize($name)); + } + + /** + * Returns whether or not the passed table is the owning side for this + * association. This means that rows in the 'target' table would miss important + * or required information if the row in 'source' did not exist. + * + * @param \Cake\ORM\Table $side The potential Table with ownership + * @return bool + */ + public function isOwningSide(Table $side) + { + return $side === $this->getTarget(); + } + + /** + * Get the relationship type. + * + * @return string + */ + public function type() + { + return self::MANY_TO_ONE; + } + + /** + * Takes an entity from the source table and looks if there is a field + * matching the property name for this association. The found entity will be + * saved on the target table for this association by passing supplied + * `$options` + * + * @param \Cake\Datasource\EntityInterface $entity an entity from the source table + * @param array $options options to be passed to the save method in the target table + * @return bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns + * the saved entity + * @see \Cake\ORM\Table::save() + */ + public function saveAssociated(EntityInterface $entity, array $options = []) + { + $targetEntity = $entity->get($this->getProperty()); + if (empty($targetEntity) || !($targetEntity instanceof EntityInterface)) { + return $entity; + } + + $table = $this->getTarget(); + $targetEntity = $table->save($targetEntity, $options); + if (!$targetEntity) { + return false; + } + + $properties = array_combine( + (array)$this->getForeignKey(), + $targetEntity->extract((array)$this->getBindingKey()) + ); + $entity->set($properties, ['guard' => false]); + + return $entity; + } + + /** + * Returns a single or multiple conditions to be appended to the generated join + * clause for getting the results on the target table. + * + * @param array $options list of options passed to attachTo method + * @return array + * @throws \RuntimeException if the number of columns in the foreignKey do not + * match the number of columns in the target table primaryKey + */ + protected function _joinCondition($options) + { + $conditions = []; + $tAlias = $this->_name; + $sAlias = $this->_sourceTable->getAlias(); + $foreignKey = (array)$options['foreignKey']; + $bindingKey = (array)$this->getBindingKey(); + + if (count($foreignKey) !== count($bindingKey)) { + if (empty($bindingKey)) { + $msg = 'The "%s" table does not define a primary key. Please set one.'; + throw new RuntimeException(sprintf($msg, $this->getTarget()->getTable())); + } + + $msg = 'Cannot match provided foreignKey for "%s", got "(%s)" but expected foreign key for "(%s)"'; + throw new RuntimeException(sprintf( + $msg, + $this->_name, + implode(', ', $foreignKey), + implode(', ', $bindingKey) + )); + } + + foreach ($foreignKey as $k => $f) { + $field = sprintf('%s.%s', $tAlias, $bindingKey[$k]); + $value = new IdentifierExpression(sprintf('%s.%s', $sAlias, $f)); + $conditions[$field] = $value; + } + + return $conditions; + } + + /** + * {@inheritDoc} + * + * @return \Closure + */ + public function eagerLoader(array $options) + { + $loader = new SelectLoader([ + 'alias' => $this->getAlias(), + 'sourceAlias' => $this->getSource()->getAlias(), + 'targetAlias' => $this->getTarget()->getAlias(), + 'foreignKey' => $this->getForeignKey(), + 'bindingKey' => $this->getBindingKey(), + 'strategy' => $this->getStrategy(), + 'associationType' => $this->type(), + 'finder' => [$this, 'find'] + ]); + + return $loader->buildEagerLoader($options); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php new file mode 100644 index 000000000..9e114ecf2 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php @@ -0,0 +1,1471 @@ +_targetForeignKey = $key; + + return $this; + } + + /** + * Gets the name of the field representing the foreign key to the target table. + * + * @return string + */ + public function getTargetForeignKey() + { + if ($this->_targetForeignKey === null) { + $this->_targetForeignKey = $this->_modelKey($this->getTarget()->getAlias()); + } + + return $this->_targetForeignKey; + } + + /** + * Sets the name of the field representing the foreign key to the target table. + * If no parameters are passed current field is returned + * + * @deprecated 3.4.0 Use setTargetForeignKey()/getTargetForeignKey() instead. + * @param string|null $key the key to be used to link both tables together + * @return string + */ + public function targetForeignKey($key = null) + { + deprecationWarning( + 'BelongToMany::targetForeignKey() is deprecated. ' . + 'Use setTargetForeignKey()/getTargetForeignKey() instead.' + ); + if ($key !== null) { + $this->setTargetForeignKey($key); + } + + return $this->getTargetForeignKey(); + } + + /** + * Whether this association can be expressed directly in a query join + * + * @param array $options custom options key that could alter the return value + * @return bool if the 'matching' key in $option is true then this function + * will return true, false otherwise + */ + public function canBeJoined(array $options = []) + { + return !empty($options['matching']); + } + + /** + * Gets the name of the field representing the foreign key to the source table. + * + * @return string + */ + public function getForeignKey() + { + if ($this->_foreignKey === null) { + $this->_foreignKey = $this->_modelKey($this->getSource()->getTable()); + } + + return $this->_foreignKey; + } + + /** + * Sets the sort order in which target records should be returned. + * + * @param mixed $sort A find() compatible order clause + * @return $this + */ + public function setSort($sort) + { + $this->_sort = $sort; + + return $this; + } + + /** + * Gets the sort order in which target records should be returned. + * + * @return mixed + */ + public function getSort() + { + return $this->_sort; + } + + /** + * Sets the sort order in which target records should be returned. + * If no arguments are passed the currently configured value is returned + * + * @deprecated 3.5.0 Use setSort()/getSort() instead. + * @param mixed $sort A find() compatible order clause + * @return mixed + */ + public function sort($sort = null) + { + deprecationWarning( + 'BelongToMany::sort() is deprecated. ' . + 'Use setSort()/getSort() instead.' + ); + if ($sort !== null) { + $this->setSort($sort); + } + + return $this->getSort(); + } + + /** + * {@inheritDoc} + */ + public function defaultRowValue($row, $joined) + { + $sourceAlias = $this->getSource()->getAlias(); + if (isset($row[$sourceAlias])) { + $row[$sourceAlias][$this->getProperty()] = $joined ? null : []; + } + + return $row; + } + + /** + * Sets the table instance for the junction relation. If no arguments + * are passed, the current configured table instance is returned + * + * @param string|\Cake\ORM\Table|null $table Name or instance for the join table + * @return \Cake\ORM\Table + */ + public function junction($table = null) + { + if ($table === null && $this->_junctionTable) { + return $this->_junctionTable; + } + + $tableLocator = $this->getTableLocator(); + if ($table === null && $this->_through) { + $table = $this->_through; + } elseif ($table === null) { + $tableName = $this->_junctionTableName(); + $tableAlias = Inflector::camelize($tableName); + + $config = []; + if (!$tableLocator->exists($tableAlias)) { + $config = ['table' => $tableName]; + + // Propagate the connection if we'll get an auto-model + if (!App::className($tableAlias, 'Model/Table', 'Table')) { + $config['connection'] = $this->getSource()->getConnection(); + } + } + $table = $tableLocator->get($tableAlias, $config); + } + + if (is_string($table)) { + $table = $tableLocator->get($table); + } + $source = $this->getSource(); + $target = $this->getTarget(); + + $this->_generateSourceAssociations($table, $source); + $this->_generateTargetAssociations($table, $source, $target); + $this->_generateJunctionAssociations($table, $source, $target); + + return $this->_junctionTable = $table; + } + + /** + * Generate reciprocal associations as necessary. + * + * Generates the following associations: + * + * - target hasMany junction e.g. Articles hasMany ArticlesTags + * - target belongsToMany source e.g Articles belongsToMany Tags. + * + * You can override these generated associations by defining associations + * with the correct aliases. + * + * @param \Cake\ORM\Table $junction The junction table. + * @param \Cake\ORM\Table $source The source table. + * @param \Cake\ORM\Table $target The target table. + * @return void + */ + protected function _generateTargetAssociations($junction, $source, $target) + { + $junctionAlias = $junction->getAlias(); + $sAlias = $source->getAlias(); + + if (!$target->hasAssociation($junctionAlias)) { + $target->hasMany($junctionAlias, [ + 'targetTable' => $junction, + 'foreignKey' => $this->getTargetForeignKey(), + 'strategy' => $this->_strategy, + ]); + } + if (!$target->hasAssociation($sAlias)) { + $target->belongsToMany($sAlias, [ + 'sourceTable' => $target, + 'targetTable' => $source, + 'foreignKey' => $this->getTargetForeignKey(), + 'targetForeignKey' => $this->getForeignKey(), + 'through' => $junction, + 'conditions' => $this->getConditions(), + 'strategy' => $this->_strategy, + ]); + } + } + + /** + * Generate additional source table associations as necessary. + * + * Generates the following associations: + * + * - source hasMany junction e.g. Tags hasMany ArticlesTags + * + * You can override these generated associations by defining associations + * with the correct aliases. + * + * @param \Cake\ORM\Table $junction The junction table. + * @param \Cake\ORM\Table $source The source table. + * @return void + */ + protected function _generateSourceAssociations($junction, $source) + { + $junctionAlias = $junction->getAlias(); + if (!$source->hasAssociation($junctionAlias)) { + $source->hasMany($junctionAlias, [ + 'targetTable' => $junction, + 'foreignKey' => $this->getForeignKey(), + 'strategy' => $this->_strategy, + ]); + } + } + + /** + * Generate associations on the junction table as necessary + * + * Generates the following associations: + * + * - junction belongsTo source e.g. ArticlesTags belongsTo Tags + * - junction belongsTo target e.g. ArticlesTags belongsTo Articles + * + * You can override these generated associations by defining associations + * with the correct aliases. + * + * @param \Cake\ORM\Table $junction The junction table. + * @param \Cake\ORM\Table $source The source table. + * @param \Cake\ORM\Table $target The target table. + * @return void + */ + protected function _generateJunctionAssociations($junction, $source, $target) + { + $tAlias = $target->getAlias(); + $sAlias = $source->getAlias(); + + if (!$junction->hasAssociation($tAlias)) { + $junction->belongsTo($tAlias, [ + 'foreignKey' => $this->getTargetForeignKey(), + 'targetTable' => $target + ]); + } + if (!$junction->hasAssociation($sAlias)) { + $junction->belongsTo($sAlias, [ + 'foreignKey' => $this->getForeignKey(), + 'targetTable' => $source + ]); + } + } + + /** + * Alters a Query object to include the associated target table data in the final + * result + * + * The options array accept the following keys: + * + * - includeFields: Whether to include target model fields in the result or not + * - foreignKey: The name of the field to use as foreign key, if false none + * will be used + * - conditions: array with a list of conditions to filter the join with + * - fields: a list of fields in the target table to include in the result + * - type: The type of join to be used (e.g. INNER) + * + * @param \Cake\ORM\Query $query the query to be altered to include the target table data + * @param array $options Any extra options or overrides to be taken in account + * @return void + */ + public function attachTo(Query $query, array $options = []) + { + if (!empty($options['negateMatch'])) { + $this->_appendNotMatching($query, $options); + + return; + } + + $junction = $this->junction(); + $belongsTo = $junction->getAssociation($this->getSource()->getAlias()); + $cond = $belongsTo->_joinCondition(['foreignKey' => $belongsTo->getForeignKey()]); + $cond += $this->junctionConditions(); + + $includeFields = null; + if (isset($options['includeFields'])) { + $includeFields = $options['includeFields']; + } + + // Attach the junction table as well we need it to populate _joinData. + $assoc = $this->_targetTable->getAssociation($junction->getAlias()); + $newOptions = array_intersect_key($options, ['joinType' => 1, 'fields' => 1]); + $newOptions += [ + 'conditions' => $cond, + 'includeFields' => $includeFields, + 'foreignKey' => false, + ]; + $assoc->attachTo($query, $newOptions); + $query->getEagerLoader()->addToJoinsMap($junction->getAlias(), $assoc, true); + + parent::attachTo($query, $options); + + $foreignKey = $this->getTargetForeignKey(); + $thisJoin = $query->clause('join')[$this->getName()]; + $thisJoin['conditions']->add($assoc->_joinCondition(['foreignKey' => $foreignKey])); + } + + /** + * {@inheritDoc} + */ + protected function _appendNotMatching($query, $options) + { + if (empty($options['negateMatch'])) { + return; + } + if (!isset($options['conditions'])) { + $options['conditions'] = []; + } + $junction = $this->junction(); + $belongsTo = $junction->getAssociation($this->getSource()->getAlias()); + $conds = $belongsTo->_joinCondition(['foreignKey' => $belongsTo->getForeignKey()]); + + $subquery = $this->find() + ->select(array_values($conds)) + ->where($options['conditions']) + ->andWhere($this->junctionConditions()); + + if (!empty($options['queryBuilder'])) { + $subquery = $options['queryBuilder']($subquery); + } + + $assoc = $junction->getAssociation($this->getTarget()->getAlias()); + $conditions = $assoc->_joinCondition([ + 'foreignKey' => $this->getTargetForeignKey() + ]); + $subquery = $this->_appendJunctionJoin($subquery, $conditions); + + $query + ->andWhere(function ($exp) use ($subquery, $conds) { + $identifiers = []; + foreach (array_keys($conds) as $field) { + $identifiers[] = new IdentifierExpression($field); + } + $identifiers = $subquery->newExpr()->add($identifiers)->setConjunction(','); + $nullExp = clone $exp; + + return $exp + ->or_([ + $exp->notIn($identifiers, $subquery), + $nullExp->and(array_map([$nullExp, 'isNull'], array_keys($conds))), + ]); + }); + } + + /** + * Get the relationship type. + * + * @return string + */ + public function type() + { + return self::MANY_TO_MANY; + } + + /** + * Return false as join conditions are defined in the junction table + * + * @param array $options list of options passed to attachTo method + * @return bool false + */ + protected function _joinCondition($options) + { + return false; + } + + /** + * {@inheritDoc} + * + * @return \Closure + */ + public function eagerLoader(array $options) + { + $name = $this->_junctionAssociationName(); + $loader = new SelectWithPivotLoader([ + 'alias' => $this->getAlias(), + 'sourceAlias' => $this->getSource()->getAlias(), + 'targetAlias' => $this->getTarget()->getAlias(), + 'foreignKey' => $this->getForeignKey(), + 'bindingKey' => $this->getBindingKey(), + 'strategy' => $this->getStrategy(), + 'associationType' => $this->type(), + 'sort' => $this->getSort(), + 'junctionAssociationName' => $name, + 'junctionProperty' => $this->_junctionProperty, + 'junctionAssoc' => $this->getTarget()->getAssociation($name), + 'junctionConditions' => $this->junctionConditions(), + 'finder' => function () { + return $this->_appendJunctionJoin($this->find(), []); + } + ]); + + return $loader->buildEagerLoader($options); + } + + /** + * Clear out the data in the junction table for a given entity. + * + * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascading delete. + * @param array $options The options for the original delete. + * @return bool Success. + */ + public function cascadeDelete(EntityInterface $entity, array $options = []) + { + if (!$this->getDependent()) { + return true; + } + $foreignKey = (array)$this->getForeignKey(); + $bindingKey = (array)$this->getBindingKey(); + $conditions = []; + + if (!empty($bindingKey)) { + $conditions = array_combine($foreignKey, $entity->extract($bindingKey)); + } + + $table = $this->junction(); + $hasMany = $this->getSource()->getAssociation($table->getAlias()); + if ($this->_cascadeCallbacks) { + foreach ($hasMany->find('all')->where($conditions)->all()->toList() as $related) { + $table->delete($related, $options); + } + + return true; + } + + $conditions = array_merge($conditions, $hasMany->getConditions()); + + $table->deleteAll($conditions); + + return true; + } + + /** + * Returns boolean true, as both of the tables 'own' rows in the other side + * of the association via the joint table. + * + * @param \Cake\ORM\Table $side The potential Table with ownership + * @return bool + */ + public function isOwningSide(Table $side) + { + return true; + } + + /** + * Sets the strategy that should be used for saving. + * + * @param string $strategy the strategy name to be used + * @throws \InvalidArgumentException if an invalid strategy name is passed + * @return $this + */ + public function setSaveStrategy($strategy) + { + if (!in_array($strategy, [self::SAVE_APPEND, self::SAVE_REPLACE])) { + $msg = sprintf('Invalid save strategy "%s"', $strategy); + throw new InvalidArgumentException($msg); + } + + $this->_saveStrategy = $strategy; + + return $this; + } + + /** + * Gets the strategy that should be used for saving. + * + * @return string the strategy to be used for saving + */ + public function getSaveStrategy() + { + return $this->_saveStrategy; + } + + /** + * Sets the strategy that should be used for saving. If called with no + * arguments, it will return the currently configured strategy + * + * @deprecated 3.4.0 Use setSaveStrategy()/getSaveStrategy() instead. + * @param string|null $strategy the strategy name to be used + * @throws \InvalidArgumentException if an invalid strategy name is passed + * @return string the strategy to be used for saving + */ + public function saveStrategy($strategy = null) + { + deprecationWarning( + 'BelongsToMany::saveStrategy() is deprecated. ' . + 'Use setSaveStrategy()/getSaveStrategy() instead.' + ); + if ($strategy !== null) { + $this->setSaveStrategy($strategy); + } + + return $this->getSaveStrategy(); + } + + /** + * Takes an entity from the source table and looks if there is a field + * matching the property name for this association. The found entity will be + * saved on the target table for this association by passing supplied + * `$options` + * + * When using the 'append' strategy, this function will only create new links + * between each side of this association. It will not destroy existing ones even + * though they may not be present in the array of entities to be saved. + * + * When using the 'replace' strategy, existing links will be removed and new links + * will be created in the joint table. If there exists links in the database to some + * of the entities intended to be saved by this method, they will be updated, + * not deleted. + * + * @param \Cake\Datasource\EntityInterface $entity an entity from the source table + * @param array $options options to be passed to the save method in the target table + * @throws \InvalidArgumentException if the property representing the association + * in the parent entity cannot be traversed + * @return bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns + * the saved entity + * @see \Cake\ORM\Table::save() + * @see \Cake\ORM\Association\BelongsToMany::replaceLinks() + */ + public function saveAssociated(EntityInterface $entity, array $options = []) + { + $targetEntity = $entity->get($this->getProperty()); + $strategy = $this->getSaveStrategy(); + + $isEmpty = in_array($targetEntity, [null, [], '', false], true); + if ($isEmpty && $entity->isNew()) { + return $entity; + } + if ($isEmpty) { + $targetEntity = []; + } + + if ($strategy === self::SAVE_APPEND) { + return $this->_saveTarget($entity, $targetEntity, $options); + } + + if ($this->replaceLinks($entity, $targetEntity, $options)) { + return $entity; + } + + return false; + } + + /** + * Persists each of the entities into the target table and creates links between + * the parent entity and each one of the saved target entities. + * + * @param \Cake\Datasource\EntityInterface $parentEntity the source entity containing the target + * entities to be saved. + * @param array|\Traversable $entities list of entities to persist in target table and to + * link to the parent entity + * @param array $options list of options accepted by `Table::save()` + * @throws \InvalidArgumentException if the property representing the association + * in the parent entity cannot be traversed + * @return \Cake\Datasource\EntityInterface|bool The parent entity after all links have been + * created if no errors happened, false otherwise + */ + protected function _saveTarget(EntityInterface $parentEntity, $entities, $options) + { + $joinAssociations = false; + if (!empty($options['associated'][$this->_junctionProperty]['associated'])) { + $joinAssociations = $options['associated'][$this->_junctionProperty]['associated']; + } + unset($options['associated'][$this->_junctionProperty]); + + if (!(is_array($entities) || $entities instanceof Traversable)) { + $name = $this->getProperty(); + $message = sprintf('Could not save %s, it cannot be traversed', $name); + throw new InvalidArgumentException($message); + } + + $table = $this->getTarget(); + $original = $entities; + $persisted = []; + + foreach ($entities as $k => $entity) { + if (!($entity instanceof EntityInterface)) { + break; + } + + if (!empty($options['atomic'])) { + $entity = clone $entity; + } + + $saved = $table->save($entity, $options); + if ($saved) { + $entities[$k] = $entity; + $persisted[] = $entity; + continue; + } + + // Saving the new linked entity failed, copy errors back into the + // original entity if applicable and abort. + if (!empty($options['atomic'])) { + $original[$k]->setErrors($entity->getErrors()); + } + if (!$saved) { + return false; + } + } + + $options['associated'] = $joinAssociations; + $success = $this->_saveLinks($parentEntity, $persisted, $options); + if (!$success && !empty($options['atomic'])) { + $parentEntity->set($this->getProperty(), $original); + + return false; + } + + $parentEntity->set($this->getProperty(), $entities); + + return $parentEntity; + } + + /** + * Creates links between the source entity and each of the passed target entities + * + * @param \Cake\Datasource\EntityInterface $sourceEntity the entity from source table in this + * association + * @param array $targetEntities list of entities to link to link to the source entity using the + * junction table + * @param array $options list of options accepted by `Table::save()` + * @return bool success + */ + protected function _saveLinks(EntityInterface $sourceEntity, $targetEntities, $options) + { + $target = $this->getTarget(); + $junction = $this->junction(); + $entityClass = $junction->getEntityClass(); + $belongsTo = $junction->getAssociation($target->getAlias()); + $foreignKey = (array)$this->getForeignKey(); + $assocForeignKey = (array)$belongsTo->getForeignKey(); + $targetPrimaryKey = (array)$target->getPrimaryKey(); + $bindingKey = (array)$this->getBindingKey(); + $jointProperty = $this->_junctionProperty; + $junctionAlias = $junction->getAlias(); + + foreach ($targetEntities as $e) { + $joint = $e->get($jointProperty); + if (!$joint || !($joint instanceof EntityInterface)) { + $joint = new $entityClass([], ['markNew' => true, 'source' => $junctionAlias]); + } + $sourceKeys = array_combine($foreignKey, $sourceEntity->extract($bindingKey)); + $targetKeys = array_combine($assocForeignKey, $e->extract($targetPrimaryKey)); + + $changedKeys = ( + $sourceKeys !== $joint->extract($foreignKey) || + $targetKeys !== $joint->extract($assocForeignKey) + ); + // Keys were changed, the junction table record _could_ be + // new. By clearing the primary key values, and marking the entity + // as new, we let save() sort out whether or not we have a new link + // or if we are updating an existing link. + if ($changedKeys) { + $joint->isNew(true); + $joint->unsetProperty($junction->getPrimaryKey()) + ->set(array_merge($sourceKeys, $targetKeys), ['guard' => false]); + } + $saved = $junction->save($joint, $options); + + if (!$saved && !empty($options['atomic'])) { + return false; + } + + $e->set($jointProperty, $joint); + $e->setDirty($jointProperty, false); + } + + return true; + } + + /** + * Associates the source entity to each of the target entities provided by + * creating links in the junction table. Both the source entity and each of + * the target entities are assumed to be already persisted, if they are marked + * as new or their status is unknown then an exception will be thrown. + * + * When using this method, all entities in `$targetEntities` will be appended to + * the source entity's property corresponding to this association object. + * + * This method does not check link uniqueness. + * + * ### Example: + * + * ``` + * $newTags = $tags->find('relevant')->toArray(); + * $articles->getAssociation('tags')->link($article, $newTags); + * ``` + * + * `$article->get('tags')` will contain all tags in `$newTags` after liking + * + * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side + * of this association + * @param array $targetEntities list of entities belonging to the `target` side + * of this association + * @param array $options list of options to be passed to the internal `save` call + * @throws \InvalidArgumentException when any of the values in $targetEntities is + * detected to not be already persisted + * @return bool true on success, false otherwise + */ + public function link(EntityInterface $sourceEntity, array $targetEntities, array $options = []) + { + $this->_checkPersistenceStatus($sourceEntity, $targetEntities); + $property = $this->getProperty(); + $links = $sourceEntity->get($property) ?: []; + $links = array_merge($links, $targetEntities); + $sourceEntity->set($property, $links); + + return $this->junction()->getConnection()->transactional( + function () use ($sourceEntity, $targetEntities, $options) { + return $this->_saveLinks($sourceEntity, $targetEntities, $options); + } + ); + } + + /** + * Removes all links between the passed source entity and each of the provided + * target entities. This method assumes that all passed objects are already persisted + * in the database and that each of them contain a primary key value. + * + * ### Options + * + * Additionally to the default options accepted by `Table::delete()`, the following + * keys are supported: + * + * - cleanProperty: Whether or not to remove all the objects in `$targetEntities` that + * are stored in `$sourceEntity` (default: true) + * + * By default this method will unset each of the entity objects stored inside the + * source entity. + * + * ### Example: + * + * ``` + * $article->tags = [$tag1, $tag2, $tag3, $tag4]; + * $tags = [$tag1, $tag2, $tag3]; + * $articles->getAssociation('tags')->unlink($article, $tags); + * ``` + * + * `$article->get('tags')` will contain only `[$tag4]` after deleting in the database + * + * @param \Cake\Datasource\EntityInterface $sourceEntity An entity persisted in the source table for + * this association. + * @param array $targetEntities List of entities persisted in the target table for + * this association. + * @param array|bool $options List of options to be passed to the internal `delete` call, + * or a `boolean` as `cleanProperty` key shortcut. + * @throws \InvalidArgumentException If non persisted entities are passed or if + * any of them is lacking a primary key value. + * @return bool Success + */ + public function unlink(EntityInterface $sourceEntity, array $targetEntities, $options = []) + { + if (is_bool($options)) { + $options = [ + 'cleanProperty' => $options + ]; + } else { + $options += ['cleanProperty' => true]; + } + + $this->_checkPersistenceStatus($sourceEntity, $targetEntities); + $property = $this->getProperty(); + + $this->junction()->getConnection()->transactional( + function () use ($sourceEntity, $targetEntities, $options) { + $links = $this->_collectJointEntities($sourceEntity, $targetEntities); + foreach ($links as $entity) { + $this->_junctionTable->delete($entity, $options); + } + } + ); + + $existing = $sourceEntity->get($property) ?: []; + if (!$options['cleanProperty'] || empty($existing)) { + return true; + } + + $storage = new SplObjectStorage(); + foreach ($targetEntities as $e) { + $storage->attach($e); + } + + foreach ($existing as $k => $e) { + if ($storage->contains($e)) { + unset($existing[$k]); + } + } + + $sourceEntity->set($property, array_values($existing)); + $sourceEntity->setDirty($property, false); + + return true; + } + + /** + * {@inheritDoc} + */ + public function setConditions($conditions) + { + parent::setConditions($conditions); + $this->_targetConditions = $this->_junctionConditions = null; + + return $this; + } + + /** + * Sets the current join table, either the name of the Table instance or the instance itself. + * + * @param string|\Cake\ORM\Table $through Name of the Table instance or the instance itself + * @return $this + */ + public function setThrough($through) + { + $this->_through = $through; + + return $this; + } + + /** + * Gets the current join table, either the name of the Table instance or the instance itself. + * + * @return string|\Cake\ORM\Table + */ + public function getThrough() + { + return $this->_through; + } + + /** + * Returns filtered conditions that reference the target table. + * + * Any string expressions, or expression objects will + * also be returned in this list. + * + * @return mixed Generally an array. If the conditions + * are not an array, the association conditions will be + * returned unmodified. + */ + protected function targetConditions() + { + if ($this->_targetConditions !== null) { + return $this->_targetConditions; + } + $conditions = $this->getConditions(); + if (!is_array($conditions)) { + return $conditions; + } + $matching = []; + $alias = $this->getAlias() . '.'; + foreach ($conditions as $field => $value) { + if (is_string($field) && strpos($field, $alias) === 0) { + $matching[$field] = $value; + } elseif (is_int($field) || $value instanceof ExpressionInterface) { + $matching[$field] = $value; + } + } + + return $this->_targetConditions = $matching; + } + + /** + * Returns filtered conditions that specifically reference + * the junction table. + * + * @return array + */ + protected function junctionConditions() + { + if ($this->_junctionConditions !== null) { + return $this->_junctionConditions; + } + $matching = []; + $conditions = $this->getConditions(); + if (!is_array($conditions)) { + return $matching; + } + $alias = $this->_junctionAssociationName() . '.'; + foreach ($conditions as $field => $value) { + $isString = is_string($field); + if ($isString && strpos($field, $alias) === 0) { + $matching[$field] = $value; + } + // Assume that operators contain junction conditions. + // Trying to manage complex conditions could result in incorrect queries. + if ($isString && in_array(strtoupper($field), ['OR', 'NOT', 'AND', 'XOR'])) { + $matching[$field] = $value; + } + } + + return $this->_junctionConditions = $matching; + } + + /** + * Proxies the finding operation to the target table's find method + * and modifies the query accordingly based of this association + * configuration. + * + * If your association includes conditions, the junction table will be + * included in the query's contained associations. + * + * @param string|array|null $type the type of query to perform, if an array is passed, + * it will be interpreted as the `$options` parameter + * @param array $options The options to for the find + * @see \Cake\ORM\Table::find() + * @return \Cake\ORM\Query + */ + public function find($type = null, array $options = []) + { + $type = $type ?: $this->getFinder(); + list($type, $opts) = $this->_extractFinder($type); + $query = $this->getTarget() + ->find($type, $options + $opts) + ->where($this->targetConditions()) + ->addDefaultTypes($this->getTarget()); + + if (!$this->junctionConditions()) { + return $query; + } + + $belongsTo = $this->junction()->getAssociation($this->getTarget()->getAlias()); + $conditions = $belongsTo->_joinCondition([ + 'foreignKey' => $this->getTargetForeignKey() + ]); + $conditions += $this->junctionConditions(); + + return $this->_appendJunctionJoin($query, $conditions); + } + + /** + * Append a join to the junction table. + * + * @param \Cake\ORM\Query $query The query to append. + * @param string|array $conditions The query conditions to use. + * @return \Cake\ORM\Query The modified query. + */ + protected function _appendJunctionJoin($query, $conditions) + { + $name = $this->_junctionAssociationName(); + $joins = $query->join(); + $matching = [ + $name => [ + 'table' => $this->junction()->getTable(), + 'conditions' => $conditions, + 'type' => QueryInterface::JOIN_TYPE_INNER + ] + ]; + + $assoc = $this->getTarget()->getAssociation($name); + $query + ->addDefaultTypes($assoc->getTarget()) + ->join($matching + $joins, [], true); + + return $query; + } + + /** + * Replaces existing association links between the source entity and the target + * with the ones passed. This method does a smart cleanup, links that are already + * persisted and present in `$targetEntities` will not be deleted, new links will + * be created for the passed target entities that are not already in the database + * and the rest will be removed. + * + * For example, if an article is linked to tags 'cake' and 'framework' and you pass + * to this method an array containing the entities for tags 'cake', 'php' and 'awesome', + * only the link for cake will be kept in database, the link for 'framework' will be + * deleted and the links for 'php' and 'awesome' will be created. + * + * Existing links are not deleted and created again, they are either left untouched + * or updated so that potential extra information stored in the joint row is not + * lost. Updating the link row can be done by making sure the corresponding passed + * target entity contains the joint property with its primary key and any extra + * information to be stored. + * + * On success, the passed `$sourceEntity` will contain `$targetEntities` as value + * in the corresponding property for this association. + * + * This method assumes that links between both the source entity and each of the + * target entities are unique. That is, for any given row in the source table there + * can only be one link in the junction table pointing to any other given row in + * the target table. + * + * Additional options for new links to be saved can be passed in the third argument, + * check `Table::save()` for information on the accepted options. + * + * ### Example: + * + * ``` + * $article->tags = [$tag1, $tag2, $tag3, $tag4]; + * $articles->save($article); + * $tags = [$tag1, $tag3]; + * $articles->getAssociation('tags')->replaceLinks($article, $tags); + * ``` + * + * `$article->get('tags')` will contain only `[$tag1, $tag3]` at the end + * + * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for + * this association + * @param array $targetEntities list of entities from the target table to be linked + * @param array $options list of options to be passed to the internal `save`/`delete` calls + * when persisting/updating new links, or deleting existing ones + * @throws \InvalidArgumentException if non persisted entities are passed or if + * any of them is lacking a primary key value + * @return bool success + */ + public function replaceLinks(EntityInterface $sourceEntity, array $targetEntities, array $options = []) + { + $bindingKey = (array)$this->getBindingKey(); + $primaryValue = $sourceEntity->extract($bindingKey); + + if (count(array_filter($primaryValue, 'strlen')) !== count($bindingKey)) { + $message = 'Could not find primary key value for source entity'; + throw new InvalidArgumentException($message); + } + + return $this->junction()->getConnection()->transactional( + function () use ($sourceEntity, $targetEntities, $primaryValue, $options) { + $foreignKey = array_map([$this->_junctionTable, 'aliasField'], (array)$this->getForeignKey()); + $hasMany = $this->getSource()->getAssociation($this->_junctionTable->getAlias()); + $existing = $hasMany->find('all') + ->where(array_combine($foreignKey, $primaryValue)); + + $associationConditions = $this->getConditions(); + if ($associationConditions) { + $existing->contain($this->getTarget()->getAlias()); + $existing->andWhere($associationConditions); + } + + $jointEntities = $this->_collectJointEntities($sourceEntity, $targetEntities); + $inserts = $this->_diffLinks($existing, $jointEntities, $targetEntities, $options); + + if ($inserts && !$this->_saveTarget($sourceEntity, $inserts, $options)) { + return false; + } + + $property = $this->getProperty(); + + if (count($inserts)) { + $inserted = array_combine( + array_keys($inserts), + (array)$sourceEntity->get($property) + ); + $targetEntities = $inserted + $targetEntities; + } + + ksort($targetEntities); + $sourceEntity->set($property, array_values($targetEntities)); + $sourceEntity->setDirty($property, false); + + return true; + } + ); + } + + /** + * Helper method used to delete the difference between the links passed in + * `$existing` and `$jointEntities`. This method will return the values from + * `$targetEntities` that were not deleted from calculating the difference. + * + * @param \Cake\ORM\Query $existing a query for getting existing links + * @param array $jointEntities link entities that should be persisted + * @param array $targetEntities entities in target table that are related to + * the `$jointEntities` + * @param array $options list of options accepted by `Table::delete()` + * @return array + */ + protected function _diffLinks($existing, $jointEntities, $targetEntities, $options = []) + { + $junction = $this->junction(); + $target = $this->getTarget(); + $belongsTo = $junction->getAssociation($target->getAlias()); + $foreignKey = (array)$this->getForeignKey(); + $assocForeignKey = (array)$belongsTo->getForeignKey(); + + $keys = array_merge($foreignKey, $assocForeignKey); + $deletes = $indexed = $present = []; + + foreach ($jointEntities as $i => $entity) { + $indexed[$i] = $entity->extract($keys); + $present[$i] = array_values($entity->extract($assocForeignKey)); + } + + foreach ($existing as $result) { + $fields = $result->extract($keys); + $found = false; + foreach ($indexed as $i => $data) { + if ($fields === $data) { + unset($indexed[$i]); + $found = true; + break; + } + } + + if (!$found) { + $deletes[] = $result; + } + } + + $primary = (array)$target->getPrimaryKey(); + $jointProperty = $this->_junctionProperty; + foreach ($targetEntities as $k => $entity) { + if (!($entity instanceof EntityInterface)) { + continue; + } + $key = array_values($entity->extract($primary)); + foreach ($present as $i => $data) { + if ($key === $data && !$entity->get($jointProperty)) { + unset($targetEntities[$k], $present[$i]); + break; + } + } + } + + if ($deletes) { + foreach ($deletes as $entity) { + $junction->delete($entity, $options); + } + } + + return $targetEntities; + } + + /** + * Throws an exception should any of the passed entities is not persisted. + * + * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side + * of this association + * @param array $targetEntities list of entities belonging to the `target` side + * of this association + * @return bool + * @throws \InvalidArgumentException + */ + protected function _checkPersistenceStatus($sourceEntity, array $targetEntities) + { + if ($sourceEntity->isNew()) { + $error = 'Source entity needs to be persisted before links can be created or removed.'; + throw new InvalidArgumentException($error); + } + + foreach ($targetEntities as $entity) { + if ($entity->isNew()) { + $error = 'Cannot link entities that have not been persisted yet.'; + throw new InvalidArgumentException($error); + } + } + + return true; + } + + /** + * Returns the list of joint entities that exist between the source entity + * and each of the passed target entities + * + * @param \Cake\Datasource\EntityInterface $sourceEntity The row belonging to the source side + * of this association. + * @param array $targetEntities The rows belonging to the target side of this + * association. + * @throws \InvalidArgumentException if any of the entities is lacking a primary + * key value + * @return array + */ + protected function _collectJointEntities($sourceEntity, $targetEntities) + { + $target = $this->getTarget(); + $source = $this->getSource(); + $junction = $this->junction(); + $jointProperty = $this->_junctionProperty; + $primary = (array)$target->getPrimaryKey(); + + $result = []; + $missing = []; + + foreach ($targetEntities as $entity) { + if (!($entity instanceof EntityInterface)) { + continue; + } + $joint = $entity->get($jointProperty); + + if (!$joint || !($joint instanceof EntityInterface)) { + $missing[] = $entity->extract($primary); + continue; + } + + $result[] = $joint; + } + + if (empty($missing)) { + return $result; + } + + $belongsTo = $junction->getAssociation($target->getAlias()); + $hasMany = $source->getAssociation($junction->getAlias()); + $foreignKey = (array)$this->getForeignKey(); + $assocForeignKey = (array)$belongsTo->getForeignKey(); + $sourceKey = $sourceEntity->extract((array)$source->getPrimaryKey()); + + foreach ($missing as $key) { + $unions[] = $hasMany->find('all') + ->where(array_combine($foreignKey, $sourceKey)) + ->andWhere(array_combine($assocForeignKey, $key)); + } + + $query = array_shift($unions); + foreach ($unions as $q) { + $query->union($q); + } + + return array_merge($result, $query->toArray()); + } + + /** + * Returns the name of the association from the target table to the junction table, + * this name is used to generate alias in the query and to later on retrieve the + * results. + * + * @return string + */ + protected function _junctionAssociationName() + { + if (!$this->_junctionAssociationName) { + $this->_junctionAssociationName = $this->getTarget() + ->getAssociation($this->junction()->getAlias()) + ->getName(); + } + + return $this->_junctionAssociationName; + } + + /** + * Sets the name of the junction table. + * If no arguments are passed the current configured name is returned. A default + * name based of the associated tables will be generated if none found. + * + * @param string|null $name The name of the junction table. + * @return string + */ + protected function _junctionTableName($name = null) + { + if ($name === null) { + if (empty($this->_junctionTableName)) { + $tablesNames = array_map('Cake\Utility\Inflector::underscore', [ + $this->getSource()->getTable(), + $this->getTarget()->getTable() + ]); + sort($tablesNames); + $this->_junctionTableName = implode('_', $tablesNames); + } + + return $this->_junctionTableName; + } + + return $this->_junctionTableName = $name; + } + + /** + * Parse extra options passed in the constructor. + * + * @param array $opts original list of options passed in constructor + * @return void + */ + protected function _options(array $opts) + { + if (!empty($opts['targetForeignKey'])) { + $this->setTargetForeignKey($opts['targetForeignKey']); + } + if (!empty($opts['joinTable'])) { + $this->_junctionTableName($opts['joinTable']); + } + if (!empty($opts['through'])) { + $this->setThrough($opts['through']); + } + if (!empty($opts['saveStrategy'])) { + $this->setSaveStrategy($opts['saveStrategy']); + } + if (isset($opts['sort'])) { + $this->setSort($opts['sort']); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/DependentDeleteHelper.php b/app/vendor/cakephp/cakephp/src/ORM/Association/DependentDeleteHelper.php new file mode 100644 index 000000000..5bd74aff6 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/DependentDeleteHelper.php @@ -0,0 +1,59 @@ +getDependent()) { + return true; + } + $table = $association->getTarget(); + $foreignKey = array_map([$association, 'aliasField'], (array)$association->getForeignKey()); + $bindingKey = (array)$association->getBindingKey(); + $conditions = array_combine($foreignKey, $entity->extract($bindingKey)); + + if ($association->getCascadeCallbacks()) { + foreach ($association->find()->where($conditions)->all()->toList() as $related) { + $table->delete($related, $options); + } + + return true; + } + $conditions = array_merge($conditions, $association->getConditions()); + + return (bool)$table->deleteAll($conditions); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/DependentDeleteTrait.php b/app/vendor/cakephp/cakephp/src/ORM/Association/DependentDeleteTrait.php new file mode 100644 index 000000000..aab6048a1 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/DependentDeleteTrait.php @@ -0,0 +1,49 @@ +cascadeDelete($this, $entity, $options); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/HasMany.php b/app/vendor/cakephp/cakephp/src/ORM/Association/HasMany.php new file mode 100644 index 000000000..84999d029 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/HasMany.php @@ -0,0 +1,707 @@ +getSource(); + } + + /** + * Sets the strategy that should be used for saving. + * + * @param string $strategy the strategy name to be used + * @throws \InvalidArgumentException if an invalid strategy name is passed + * @return $this + */ + public function setSaveStrategy($strategy) + { + if (!in_array($strategy, [self::SAVE_APPEND, self::SAVE_REPLACE])) { + $msg = sprintf('Invalid save strategy "%s"', $strategy); + throw new InvalidArgumentException($msg); + } + + $this->_saveStrategy = $strategy; + + return $this; + } + + /** + * Gets the strategy that should be used for saving. + * + * @return string the strategy to be used for saving + */ + public function getSaveStrategy() + { + return $this->_saveStrategy; + } + + /** + * Sets the strategy that should be used for saving. If called with no + * arguments, it will return the currently configured strategy + * + * @deprecated 3.4.0 Use setSaveStrategy()/getSaveStrategy() instead. + * @param string|null $strategy the strategy name to be used + * @throws \InvalidArgumentException if an invalid strategy name is passed + * @return string the strategy to be used for saving + */ + public function saveStrategy($strategy = null) + { + deprecationWarning( + 'HasMany::saveStrategy() is deprecated. ' . + 'Use setSaveStrategy()/getSaveStrategy() instead.' + ); + if ($strategy !== null) { + $this->setSaveStrategy($strategy); + } + + return $this->getSaveStrategy(); + } + + /** + * Takes an entity from the source table and looks if there is a field + * matching the property name for this association. The found entity will be + * saved on the target table for this association by passing supplied + * `$options` + * + * @param \Cake\Datasource\EntityInterface $entity an entity from the source table + * @param array $options options to be passed to the save method in the target table + * @return bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns + * the saved entity + * @see \Cake\ORM\Table::save() + * @throws \InvalidArgumentException when the association data cannot be traversed. + */ + public function saveAssociated(EntityInterface $entity, array $options = []) + { + $targetEntities = $entity->get($this->getProperty()); + + $isEmpty = in_array($targetEntities, [null, [], '', false], true); + if ($isEmpty) { + if ($entity->isNew() || + $this->getSaveStrategy() !== self::SAVE_REPLACE + ) { + return $entity; + } + + $targetEntities = []; + } + + if (!is_array($targetEntities) && + !($targetEntities instanceof Traversable) + ) { + $name = $this->getProperty(); + $message = sprintf('Could not save %s, it cannot be traversed', $name); + throw new InvalidArgumentException($message); + } + + $foreignKeyReference = array_combine( + (array)$this->getForeignKey(), + $entity->extract((array)$this->getBindingKey()) + ); + + $options['_sourceTable'] = $this->getSource(); + + if ($this->_saveStrategy === self::SAVE_REPLACE && + !$this->_unlinkAssociated($foreignKeyReference, $entity, $this->getTarget(), $targetEntities, $options) + ) { + return false; + } + + if (!$this->_saveTarget($foreignKeyReference, $entity, $targetEntities, $options)) { + return false; + } + + return $entity; + } + + /** + * Persists each of the entities into the target table and creates links between + * the parent entity and each one of the saved target entities. + * + * @param array $foreignKeyReference The foreign key reference defining the link between the + * target entity, and the parent entity. + * @param \Cake\Datasource\EntityInterface $parentEntity The source entity containing the target + * entities to be saved. + * @param array|\Traversable $entities list of entities to persist in target table and to + * link to the parent entity + * @param array $options list of options accepted by `Table::save()`. + * @return bool `true` on success, `false` otherwise. + */ + protected function _saveTarget(array $foreignKeyReference, EntityInterface $parentEntity, $entities, array $options) + { + $foreignKey = array_keys($foreignKeyReference); + $table = $this->getTarget(); + $original = $entities; + + foreach ($entities as $k => $entity) { + if (!($entity instanceof EntityInterface)) { + break; + } + + if (!empty($options['atomic'])) { + $entity = clone $entity; + } + + if ($foreignKeyReference !== $entity->extract($foreignKey)) { + $entity->set($foreignKeyReference, ['guard' => false]); + } + + if ($table->save($entity, $options)) { + $entities[$k] = $entity; + continue; + } + + if (!empty($options['atomic'])) { + $original[$k]->setErrors($entity->getErrors()); + $entity->set($this->getProperty(), $original); + + return false; + } + } + + $parentEntity->set($this->getProperty(), $entities); + + return true; + } + + /** + * Associates the source entity to each of the target entities provided. + * When using this method, all entities in `$targetEntities` will be appended to + * the source entity's property corresponding to this association object. + * + * This method does not check link uniqueness. + * Changes are persisted in the database and also in the source entity. + * + * ### Example: + * + * ``` + * $user = $users->get(1); + * $allArticles = $articles->find('all')->toArray(); + * $users->Articles->link($user, $allArticles); + * ``` + * + * `$user->get('articles')` will contain all articles in `$allArticles` after linking + * + * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side + * of this association + * @param array $targetEntities list of entities belonging to the `target` side + * of this association + * @param array $options list of options to be passed to the internal `save` call + * @return bool true on success, false otherwise + */ + public function link(EntityInterface $sourceEntity, array $targetEntities, array $options = []) + { + $saveStrategy = $this->getSaveStrategy(); + $this->setSaveStrategy(self::SAVE_APPEND); + $property = $this->getProperty(); + + $currentEntities = array_unique( + array_merge( + (array)$sourceEntity->get($property), + $targetEntities + ) + ); + + $sourceEntity->set($property, $currentEntities); + + $savedEntity = $this->getConnection()->transactional(function () use ($sourceEntity, $options) { + return $this->saveAssociated($sourceEntity, $options); + }); + + $ok = ($savedEntity instanceof EntityInterface); + + $this->setSaveStrategy($saveStrategy); + + if ($ok) { + $sourceEntity->set($property, $savedEntity->get($property)); + $sourceEntity->setDirty($property, false); + } + + return $ok; + } + + /** + * Removes all links between the passed source entity and each of the provided + * target entities. This method assumes that all passed objects are already persisted + * in the database and that each of them contain a primary key value. + * + * ### Options + * + * Additionally to the default options accepted by `Table::delete()`, the following + * keys are supported: + * + * - cleanProperty: Whether or not to remove all the objects in `$targetEntities` that + * are stored in `$sourceEntity` (default: true) + * + * By default this method will unset each of the entity objects stored inside the + * source entity. + * + * Changes are persisted in the database and also in the source entity. + * + * ### Example: + * + * ``` + * $user = $users->get(1); + * $user->articles = [$article1, $article2, $article3, $article4]; + * $users->save($user, ['Associated' => ['Articles']]); + * $allArticles = [$article1, $article2, $article3]; + * $users->Articles->unlink($user, $allArticles); + * ``` + * + * `$article->get('articles')` will contain only `[$article4]` after deleting in the database + * + * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for + * this association + * @param array $targetEntities list of entities persisted in the target table for + * this association + * @param array $options list of options to be passed to the internal `delete` call + * @throws \InvalidArgumentException if non persisted entities are passed or if + * any of them is lacking a primary key value + * @return void + */ + public function unlink(EntityInterface $sourceEntity, array $targetEntities, $options = []) + { + if (is_bool($options)) { + $options = [ + 'cleanProperty' => $options + ]; + } else { + $options += ['cleanProperty' => true]; + } + if (count($targetEntities) === 0) { + return; + } + + $foreignKey = (array)$this->getForeignKey(); + $target = $this->getTarget(); + $targetPrimaryKey = array_merge((array)$target->getPrimaryKey(), $foreignKey); + $property = $this->getProperty(); + + $conditions = [ + 'OR' => (new Collection($targetEntities)) + ->map(function ($entity) use ($targetPrimaryKey) { + return $entity->extract($targetPrimaryKey); + }) + ->toList() + ]; + + $this->_unlink($foreignKey, $target, $conditions, $options); + + $result = $sourceEntity->get($property); + if ($options['cleanProperty'] && $result !== null) { + $sourceEntity->set( + $property, + (new Collection($sourceEntity->get($property))) + ->reject( + function ($assoc) use ($targetEntities) { + return in_array($assoc, $targetEntities); + } + ) + ->toList() + ); + } + + $sourceEntity->setDirty($property, false); + } + + /** + * Replaces existing association links between the source entity and the target + * with the ones passed. This method does a smart cleanup, links that are already + * persisted and present in `$targetEntities` will not be deleted, new links will + * be created for the passed target entities that are not already in the database + * and the rest will be removed. + * + * For example, if an author has many articles, such as 'article1','article 2' and 'article 3' and you pass + * to this method an array containing the entities for articles 'article 1' and 'article 4', + * only the link for 'article 1' will be kept in database, the links for 'article 2' and 'article 3' will be + * deleted and the link for 'article 4' will be created. + * + * Existing links are not deleted and created again, they are either left untouched + * or updated. + * + * This method does not check link uniqueness. + * + * On success, the passed `$sourceEntity` will contain `$targetEntities` as value + * in the corresponding property for this association. + * + * Additional options for new links to be saved can be passed in the third argument, + * check `Table::save()` for information on the accepted options. + * + * ### Example: + * + * ``` + * $author->articles = [$article1, $article2, $article3, $article4]; + * $authors->save($author); + * $articles = [$article1, $article3]; + * $authors->getAssociation('articles')->replace($author, $articles); + * ``` + * + * `$author->get('articles')` will contain only `[$article1, $article3]` at the end + * + * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for + * this association + * @param array $targetEntities list of entities from the target table to be linked + * @param array $options list of options to be passed to the internal `save`/`delete` calls + * when persisting/updating new links, or deleting existing ones + * @throws \InvalidArgumentException if non persisted entities are passed or if + * any of them is lacking a primary key value + * @return bool success + */ + public function replace(EntityInterface $sourceEntity, array $targetEntities, array $options = []) + { + $property = $this->getProperty(); + $sourceEntity->set($property, $targetEntities); + $saveStrategy = $this->getSaveStrategy(); + $this->setSaveStrategy(self::SAVE_REPLACE); + $result = $this->saveAssociated($sourceEntity, $options); + $ok = ($result instanceof EntityInterface); + + if ($ok) { + $sourceEntity = $result; + } + $this->setSaveStrategy($saveStrategy); + + return $ok; + } + + /** + * Deletes/sets null the related objects according to the dependency between source and targets and foreign key nullability + * Skips deleting records present in $remainingEntities + * + * @param array $foreignKeyReference The foreign key reference defining the link between the + * target entity, and the parent entity. + * @param \Cake\Datasource\EntityInterface $entity the entity which should have its associated entities unassigned + * @param \Cake\ORM\Table $target The associated table + * @param array $remainingEntities Entities that should not be deleted + * @param array $options list of options accepted by `Table::delete()` + * @return bool success + */ + protected function _unlinkAssociated(array $foreignKeyReference, EntityInterface $entity, Table $target, array $remainingEntities = [], array $options = []) + { + $primaryKey = (array)$target->getPrimaryKey(); + $exclusions = new Collection($remainingEntities); + $exclusions = $exclusions->map( + function ($ent) use ($primaryKey) { + return $ent->extract($primaryKey); + } + ) + ->filter( + function ($v) { + return !in_array(null, $v, true); + } + ) + ->toArray(); + + $conditions = $foreignKeyReference; + + if (count($exclusions) > 0) { + $conditions = [ + 'NOT' => [ + 'OR' => $exclusions + ], + $foreignKeyReference + ]; + } + + return $this->_unlink(array_keys($foreignKeyReference), $target, $conditions, $options); + } + + /** + * Deletes/sets null the related objects matching $conditions. + * The action which is taken depends on the dependency between source and targets and also on foreign key nullability + * + * @param array $foreignKey array of foreign key properties + * @param \Cake\ORM\Table $target The associated table + * @param array $conditions The conditions that specifies what are the objects to be unlinked + * @param array $options list of options accepted by `Table::delete()` + * @return bool success + */ + protected function _unlink(array $foreignKey, Table $target, array $conditions = [], array $options = []) + { + $mustBeDependent = (!$this->_foreignKeyAcceptsNull($target, $foreignKey) || $this->getDependent()); + + if ($mustBeDependent) { + if ($this->_cascadeCallbacks) { + $conditions = new QueryExpression($conditions); + $conditions->traverse(function ($entry) use ($target) { + if ($entry instanceof FieldInterface) { + $entry->setField($target->aliasField($entry->getField())); + } + }); + $query = $this->find('all')->where($conditions); + $ok = true; + foreach ($query as $assoc) { + $ok = $ok && $target->delete($assoc, $options); + } + + return $ok; + } + + $conditions = array_merge($conditions, $this->getConditions()); + $target->deleteAll($conditions); + + return true; + } + + $updateFields = array_fill_keys($foreignKey, null); + $conditions = array_merge($conditions, $this->getConditions()); + $target->updateAll($updateFields, $conditions); + + return true; + } + + /** + * Checks the nullable flag of the foreign key + * + * @param \Cake\ORM\Table $table the table containing the foreign key + * @param array $properties the list of fields that compose the foreign key + * @return bool + */ + protected function _foreignKeyAcceptsNull(Table $table, array $properties) + { + return !in_array( + false, + array_map( + function ($prop) use ($table) { + return $table->getSchema()->isNullable($prop); + }, + $properties + ) + ); + } + + /** + * Get the relationship type. + * + * @return string + */ + public function type() + { + return self::ONE_TO_MANY; + } + + /** + * Whether this association can be expressed directly in a query join + * + * @param array $options custom options key that could alter the return value + * @return bool if the 'matching' key in $option is true then this function + * will return true, false otherwise + */ + public function canBeJoined(array $options = []) + { + return !empty($options['matching']); + } + + /** + * Gets the name of the field representing the foreign key to the source table. + * + * @return string + */ + public function getForeignKey() + { + if ($this->_foreignKey === null) { + $this->_foreignKey = $this->_modelKey($this->getSource()->getTable()); + } + + return $this->_foreignKey; + } + + /** + * Sets the sort order in which target records should be returned. + * + * @param mixed $sort A find() compatible order clause + * @return $this + */ + public function setSort($sort) + { + $this->_sort = $sort; + + return $this; + } + + /** + * Gets the sort order in which target records should be returned. + * + * @return mixed + */ + public function getSort() + { + return $this->_sort; + } + + /** + * Sets the sort order in which target records should be returned. + * If no arguments are passed the currently configured value is returned + * + * @deprecated 3.4.0 Use setSort()/getSort() instead. + * @param mixed $sort A find() compatible order clause + * @return mixed + */ + public function sort($sort = null) + { + deprecationWarning( + 'HasMany::sort() is deprecated. ' . + 'Use setSort()/getSort() instead.' + ); + if ($sort !== null) { + $this->setSort($sort); + } + + return $this->getSort(); + } + + /** + * {@inheritDoc} + */ + public function defaultRowValue($row, $joined) + { + $sourceAlias = $this->getSource()->getAlias(); + if (isset($row[$sourceAlias])) { + $row[$sourceAlias][$this->getProperty()] = $joined ? null : []; + } + + return $row; + } + + /** + * Parse extra options passed in the constructor. + * + * @param array $opts original list of options passed in constructor + * @return void + */ + protected function _options(array $opts) + { + if (!empty($opts['saveStrategy'])) { + $this->setSaveStrategy($opts['saveStrategy']); + } + if (isset($opts['sort'])) { + $this->setSort($opts['sort']); + } + } + + /** + * {@inheritDoc} + * + * @return \Closure + */ + public function eagerLoader(array $options) + { + $loader = new SelectLoader([ + 'alias' => $this->getAlias(), + 'sourceAlias' => $this->getSource()->getAlias(), + 'targetAlias' => $this->getTarget()->getAlias(), + 'foreignKey' => $this->getForeignKey(), + 'bindingKey' => $this->getBindingKey(), + 'strategy' => $this->getStrategy(), + 'associationType' => $this->type(), + 'sort' => $this->getSort(), + 'finder' => [$this, 'find'] + ]); + + return $loader->buildEagerLoader($options); + } + + /** + * {@inheritDoc} + */ + public function cascadeDelete(EntityInterface $entity, array $options = []) + { + $helper = new DependentDeleteHelper(); + + return $helper->cascadeDelete($this, $entity, $options); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/HasOne.php b/app/vendor/cakephp/cakephp/src/ORM/Association/HasOne.php new file mode 100644 index 000000000..88e6b49f4 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/HasOne.php @@ -0,0 +1,155 @@ +_foreignKey === null) { + $this->_foreignKey = $this->_modelKey($this->getSource()->getAlias()); + } + + return $this->_foreignKey; + } + + /** + * Returns default property name based on association name. + * + * @return string + */ + protected function _propertyName() + { + list(, $name) = pluginSplit($this->_name); + + return Inflector::underscore(Inflector::singularize($name)); + } + + /** + * Returns whether or not the passed table is the owning side for this + * association. This means that rows in the 'target' table would miss important + * or required information if the row in 'source' did not exist. + * + * @param \Cake\ORM\Table $side The potential Table with ownership + * @return bool + */ + public function isOwningSide(Table $side) + { + return $side === $this->getSource(); + } + + /** + * Get the relationship type. + * + * @return string + */ + public function type() + { + return self::ONE_TO_ONE; + } + + /** + * Takes an entity from the source table and looks if there is a field + * matching the property name for this association. The found entity will be + * saved on the target table for this association by passing supplied + * `$options` + * + * @param \Cake\Datasource\EntityInterface $entity an entity from the source table + * @param array $options options to be passed to the save method in the target table + * @return bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns + * the saved entity + * @see \Cake\ORM\Table::save() + */ + public function saveAssociated(EntityInterface $entity, array $options = []) + { + $targetEntity = $entity->get($this->getProperty()); + if (empty($targetEntity) || !($targetEntity instanceof EntityInterface)) { + return $entity; + } + + $properties = array_combine( + (array)$this->getForeignKey(), + $entity->extract((array)$this->getBindingKey()) + ); + $targetEntity->set($properties, ['guard' => false]); + + if (!$this->getTarget()->save($targetEntity, $options)) { + $targetEntity->unsetProperty(array_keys($properties)); + + return false; + } + + return $entity; + } + + /** + * {@inheritDoc} + * + * @return \Closure + */ + public function eagerLoader(array $options) + { + $loader = new SelectLoader([ + 'alias' => $this->getAlias(), + 'sourceAlias' => $this->getSource()->getAlias(), + 'targetAlias' => $this->getTarget()->getAlias(), + 'foreignKey' => $this->getForeignKey(), + 'bindingKey' => $this->getBindingKey(), + 'strategy' => $this->getStrategy(), + 'associationType' => $this->type(), + 'finder' => [$this, 'find'] + ]); + + return $loader->buildEagerLoader($options); + } + + /** + * {@inheritDoc} + */ + public function cascadeDelete(EntityInterface $entity, array $options = []) + { + $helper = new DependentDeleteHelper(); + + return $helper->cascadeDelete($this, $entity, $options); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectLoader.php b/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectLoader.php new file mode 100644 index 000000000..2c01816da --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectLoader.php @@ -0,0 +1,547 @@ +alias = $options['alias']; + $this->sourceAlias = $options['sourceAlias']; + $this->targetAlias = $options['targetAlias']; + $this->foreignKey = $options['foreignKey']; + $this->strategy = $options['strategy']; + $this->bindingKey = $options['bindingKey']; + $this->finder = $options['finder']; + $this->associationType = $options['associationType']; + $this->sort = isset($options['sort']) ? $options['sort'] : null; + } + + /** + * Returns a callable that can be used for injecting association results into a given + * iterator. The options accepted by this method are the same as `Association::eagerLoader()` + * + * @param array $options Same options as `Association::eagerLoader()` + * @return \Closure + */ + public function buildEagerLoader(array $options) + { + $options += $this->_defaultOptions(); + $fetchQuery = $this->_buildQuery($options); + $resultMap = $this->_buildResultMap($fetchQuery, $options); + + return $this->_resultInjector($fetchQuery, $resultMap, $options); + } + + /** + * Returns the default options to use for the eagerLoader + * + * @return array + */ + protected function _defaultOptions() + { + return [ + 'foreignKey' => $this->foreignKey, + 'conditions' => [], + 'strategy' => $this->strategy, + 'nestKey' => $this->alias, + 'sort' => $this->sort + ]; + } + + /** + * Auxiliary function to construct a new Query object to return all the records + * in the target table that are associated to those specified in $options from + * the source table + * + * @param array $options options accepted by eagerLoader() + * @return \Cake\ORM\Query + * @throws \InvalidArgumentException When a key is required for associations but not selected. + */ + protected function _buildQuery($options) + { + $key = $this->_linkField($options); + $filter = $options['keys']; + $useSubquery = $options['strategy'] === Association::STRATEGY_SUBQUERY; + $finder = $this->finder; + + if (!isset($options['fields'])) { + $options['fields'] = []; + } + + /* @var \Cake\ORM\Query $query */ + $query = $finder(); + if (isset($options['finder'])) { + list($finderName, $opts) = $this->_extractFinder($options['finder']); + $query = $query->find($finderName, $opts); + } + + $fetchQuery = $query + ->select($options['fields']) + ->where($options['conditions']) + ->eagerLoaded(true) + ->enableHydration($options['query']->isHydrationEnabled()); + + if ($useSubquery) { + $filter = $this->_buildSubquery($options['query']); + $fetchQuery = $this->_addFilteringJoin($fetchQuery, $key, $filter); + } else { + $fetchQuery = $this->_addFilteringCondition($fetchQuery, $key, $filter); + } + + if (!empty($options['sort'])) { + $fetchQuery->order($options['sort']); + } + + if (!empty($options['contain'])) { + $fetchQuery->contain($options['contain']); + } + + if (!empty($options['queryBuilder'])) { + $fetchQuery = $options['queryBuilder']($fetchQuery); + } + + $this->_assertFieldsPresent($fetchQuery, (array)$key); + + return $fetchQuery; + } + + /** + * Helper method to infer the requested finder and its options. + * + * Returns the inferred options from the finder $type. + * + * ### Examples: + * + * The following will call the finder 'translations' with the value of the finder as its options: + * $query->contain(['Comments' => ['finder' => ['translations']]]); + * $query->contain(['Comments' => ['finder' => ['translations' => []]]]); + * $query->contain(['Comments' => ['finder' => ['translations' => ['locales' => ['en_US']]]]]); + * + * @param string|array $finderData The finder name or an array having the name as key + * and options as value. + * @return array + */ + protected function _extractFinder($finderData) + { + $finderData = (array)$finderData; + + if (is_numeric(key($finderData))) { + return [current($finderData), []]; + } + + return [key($finderData), current($finderData)]; + } + + /** + * Checks that the fetching query either has auto fields on or + * has the foreignKey fields selected. + * If the required fields are missing, throws an exception. + * + * @param \Cake\ORM\Query $fetchQuery The association fetching query + * @param array $key The foreign key fields to check + * @return void + * @throws \InvalidArgumentException + */ + protected function _assertFieldsPresent($fetchQuery, $key) + { + $select = $fetchQuery->aliasFields($fetchQuery->clause('select')); + if (empty($select)) { + return; + } + $missingKey = function ($fieldList, $key) { + foreach ($key as $keyField) { + if (!in_array($keyField, $fieldList, true)) { + return true; + } + } + + return false; + }; + + $missingFields = $missingKey($select, $key); + if ($missingFields) { + $driver = $fetchQuery->getConnection()->getDriver(); + $quoted = array_map([$driver, 'quoteIdentifier'], $key); + $missingFields = $missingKey($select, $quoted); + } + + if ($missingFields) { + throw new InvalidArgumentException( + sprintf( + 'You are required to select the "%s" field(s)', + implode(', ', (array)$key) + ) + ); + } + } + + /** + * Appends any conditions required to load the relevant set of records in the + * target table query given a filter key and some filtering values when the + * filtering needs to be done using a subquery. + * + * @param \Cake\ORM\Query $query Target table's query + * @param string|array $key the fields that should be used for filtering + * @param \Cake\ORM\Query $subquery The Subquery to use for filtering + * @return \Cake\ORM\Query + */ + protected function _addFilteringJoin($query, $key, $subquery) + { + $filter = []; + $aliasedTable = $this->sourceAlias; + + foreach ($subquery->clause('select') as $aliasedField => $field) { + if (is_int($aliasedField)) { + $filter[] = new IdentifierExpression($field); + } else { + $filter[$aliasedField] = $field; + } + } + $subquery->select($filter, true); + + $conditions = null; + if (is_array($key)) { + $conditions = $this->_createTupleCondition($query, $key, $filter, '='); + } else { + $filter = current($filter); + } + + $conditions = $conditions ?: $query->newExpr([$key => $filter]); + + return $query->innerJoin( + [$aliasedTable => $subquery], + $conditions + ); + } + + /** + * Appends any conditions required to load the relevant set of records in the + * target table query given a filter key and some filtering values. + * + * @param \Cake\ORM\Query $query Target table's query + * @param string|array $key The fields that should be used for filtering + * @param mixed $filter The value that should be used to match for $key + * @return \Cake\ORM\Query + */ + protected function _addFilteringCondition($query, $key, $filter) + { + if (is_array($key)) { + $conditions = $this->_createTupleCondition($query, $key, $filter, 'IN'); + } + + $conditions = isset($conditions) ? $conditions : [$key . ' IN' => $filter]; + + return $query->andWhere($conditions); + } + + /** + * Returns a TupleComparison object that can be used for matching all the fields + * from $keys with the tuple values in $filter using the provided operator. + * + * @param \Cake\ORM\Query $query Target table's query + * @param array $keys the fields that should be used for filtering + * @param mixed $filter the value that should be used to match for $key + * @param string $operator The operator for comparing the tuples + * @return \Cake\Database\Expression\TupleComparison + */ + protected function _createTupleCondition($query, $keys, $filter, $operator) + { + $types = []; + $defaults = $query->getDefaultTypes(); + foreach ($keys as $k) { + if (isset($defaults[$k])) { + $types[] = $defaults[$k]; + } + } + + return new TupleComparison($keys, $filter, $types, $operator); + } + + /** + * Generates a string used as a table field that contains the values upon + * which the filter should be applied + * + * @param array $options The options for getting the link field. + * @return string|array + */ + protected function _linkField($options) + { + $links = []; + $name = $this->alias; + + if ($options['foreignKey'] === false && $this->associationType === Association::ONE_TO_MANY) { + $msg = 'Cannot have foreignKey = false for hasMany associations. ' . + 'You must provide a foreignKey column.'; + throw new RuntimeException($msg); + } + + $keys = in_array($this->associationType, [Association::ONE_TO_ONE, Association::ONE_TO_MANY]) ? + $this->foreignKey : + $this->bindingKey; + + foreach ((array)$keys as $key) { + $links[] = sprintf('%s.%s', $name, $key); + } + + if (count($links) === 1) { + return $links[0]; + } + + return $links; + } + + /** + * Builds a query to be used as a condition for filtering records in the + * target table, it is constructed by cloning the original query that was used + * to load records in the source table. + * + * @param \Cake\ORM\Query $query the original query used to load source records + * @return \Cake\ORM\Query + */ + protected function _buildSubquery($query) + { + $filterQuery = clone $query; + $filterQuery->enableAutoFields(false); + $filterQuery->mapReduce(null, null, true); + $filterQuery->formatResults(null, true); + $filterQuery->contain([], true); + $filterQuery->setValueBinder(new ValueBinder()); + + if (!$filterQuery->clause('limit')) { + $filterQuery->limit(null); + $filterQuery->order([], true); + $filterQuery->offset(null); + } + + $fields = $this->_subqueryFields($query); + $filterQuery->select($fields['select'], true)->group($fields['group']); + + return $filterQuery; + } + + /** + * Calculate the fields that need to participate in a subquery. + * + * Normally this includes the binding key columns. If there is a an ORDER BY, + * those columns are also included as the fields may be calculated or constant values, + * that need to be present to ensure the correct association data is loaded. + * + * @param \Cake\ORM\Query $query The query to get fields from. + * @return array The list of fields for the subquery. + */ + protected function _subqueryFields($query) + { + $keys = (array)$this->bindingKey; + + if ($this->associationType === Association::MANY_TO_ONE) { + $keys = (array)$this->foreignKey; + } + + $fields = $query->aliasFields($keys, $this->sourceAlias); + $group = $fields = array_values($fields); + + $order = $query->clause('order'); + if ($order) { + $columns = $query->clause('select'); + $order->iterateParts(function ($direction, $field) use (&$fields, $columns) { + if (isset($columns[$field])) { + $fields[$field] = $columns[$field]; + } + }); + } + + return ['select' => $fields, 'group' => $group]; + } + + /** + * Builds an array containing the results from fetchQuery indexed by + * the foreignKey value corresponding to this association. + * + * @param \Cake\ORM\Query $fetchQuery The query to get results from + * @param array $options The options passed to the eager loader + * @return array + */ + protected function _buildResultMap($fetchQuery, $options) + { + $resultMap = []; + $singleResult = in_array($this->associationType, [Association::MANY_TO_ONE, Association::ONE_TO_ONE]); + $keys = in_array($this->associationType, [Association::ONE_TO_ONE, Association::ONE_TO_MANY]) ? + $this->foreignKey : + $this->bindingKey; + $key = (array)$keys; + + foreach ($fetchQuery->all() as $result) { + $values = []; + foreach ($key as $k) { + $values[] = $result[$k]; + } + if ($singleResult) { + $resultMap[implode(';', $values)] = $result; + } else { + $resultMap[implode(';', $values)][] = $result; + } + } + + return $resultMap; + } + + /** + * Returns a callable to be used for each row in a query result set + * for injecting the eager loaded rows + * + * @param \Cake\ORM\Query $fetchQuery the Query used to fetch results + * @param array $resultMap an array with the foreignKey as keys and + * the corresponding target table results as value. + * @param array $options The options passed to the eagerLoader method + * @return \Closure + */ + protected function _resultInjector($fetchQuery, $resultMap, $options) + { + $keys = $this->associationType === Association::MANY_TO_ONE ? + $this->foreignKey : + $this->bindingKey; + + $sourceKeys = []; + foreach ((array)$keys as $key) { + $f = $fetchQuery->aliasField($key, $this->sourceAlias); + $sourceKeys[] = key($f); + } + + $nestKey = $options['nestKey']; + if (count($sourceKeys) > 1) { + return $this->_multiKeysInjector($resultMap, $sourceKeys, $nestKey); + } + + $sourceKey = $sourceKeys[0]; + + return function ($row) use ($resultMap, $sourceKey, $nestKey) { + if (isset($row[$sourceKey], $resultMap[$row[$sourceKey]])) { + $row[$nestKey] = $resultMap[$row[$sourceKey]]; + } + + return $row; + }; + } + + /** + * Returns a callable to be used for each row in a query result set + * for injecting the eager loaded rows when the matching needs to + * be done with multiple foreign keys + * + * @param array $resultMap A keyed arrays containing the target table + * @param array $sourceKeys An array with aliased keys to match + * @param string $nestKey The key under which results should be nested + * @return \Closure + */ + protected function _multiKeysInjector($resultMap, $sourceKeys, $nestKey) + { + return function ($row) use ($resultMap, $sourceKeys, $nestKey) { + $values = []; + foreach ($sourceKeys as $key) { + $values[] = $row[$key]; + } + + $key = implode(';', $values); + if (isset($resultMap[$key])) { + $row[$nestKey] = $resultMap[$key]; + } + + return $row; + }; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectWithPivotLoader.php b/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectWithPivotLoader.php new file mode 100644 index 000000000..733a8c7fa --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectWithPivotLoader.php @@ -0,0 +1,185 @@ +junctionAssociationName = $options['junctionAssociationName']; + $this->junctionProperty = $options['junctionProperty']; + $this->junctionAssoc = $options['junctionAssoc']; + $this->junctionConditions = $options['junctionConditions']; + } + + /** + * Auxiliary function to construct a new Query object to return all the records + * in the target table that are associated to those specified in $options from + * the source table. + * + * This is used for eager loading records on the target table based on conditions. + * + * @param array $options options accepted by eagerLoader() + * @return \Cake\ORM\Query + * @throws \InvalidArgumentException When a key is required for associations but not selected. + */ + protected function _buildQuery($options) + { + $name = $this->junctionAssociationName; + $assoc = $this->junctionAssoc; + $queryBuilder = false; + + if (!empty($options['queryBuilder'])) { + $queryBuilder = $options['queryBuilder']; + unset($options['queryBuilder']); + } + + $query = parent::_buildQuery($options); + + if ($queryBuilder) { + $query = $queryBuilder($query); + } + + if ($query->isAutoFieldsEnabled() === null) { + $query->enableAutoFields($query->clause('select') === []); + } + + // Ensure that association conditions are applied + // and that the required keys are in the selected columns. + + $tempName = $this->alias . '_CJoin'; + $schema = $assoc->getSchema(); + $joinFields = $types = []; + + foreach ($schema->typeMap() as $f => $type) { + $key = $tempName . '__' . $f; + $joinFields[$key] = "$name.$f"; + $types[$key] = $type; + } + + $query + ->where($this->junctionConditions) + ->select($joinFields); + + $query + ->getEagerLoader() + ->addToJoinsMap($tempName, $assoc, false, $this->junctionProperty); + + $assoc->attachTo($query, [ + 'aliasPath' => $assoc->getAlias(), + 'includeFields' => false, + 'propertyPath' => $this->junctionProperty, + ]); + $query->getTypeMap()->addDefaults($types); + + return $query; + } + + /** + * Generates a string used as a table field that contains the values upon + * which the filter should be applied + * + * @param array $options the options to use for getting the link field. + * @return array|string + */ + protected function _linkField($options) + { + $links = []; + $name = $this->junctionAssociationName; + + foreach ((array)$options['foreignKey'] as $key) { + $links[] = sprintf('%s.%s', $name, $key); + } + + if (count($links) === 1) { + return $links[0]; + } + + return $links; + } + + /** + * Builds an array containing the results from fetchQuery indexed by + * the foreignKey value corresponding to this association. + * + * @param \Cake\ORM\Query $fetchQuery The query to get results from + * @param array $options The options passed to the eager loader + * @return array + * @throws \RuntimeException when the association property is not part of the results set. + */ + protected function _buildResultMap($fetchQuery, $options) + { + $resultMap = []; + $key = (array)$options['foreignKey']; + + foreach ($fetchQuery->all() as $result) { + if (!isset($result[$this->junctionProperty])) { + throw new RuntimeException(sprintf( + '"%s" is missing from the belongsToMany results. Results cannot be created.', + $this->junctionProperty + )); + } + + $values = []; + foreach ($key as $k) { + $values[] = $result[$this->junctionProperty][$k]; + } + $resultMap[implode(';', $values)][] = $result; + } + + return $resultMap; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/AssociationCollection.php b/app/vendor/cakephp/cakephp/src/ORM/AssociationCollection.php new file mode 100644 index 000000000..574648f0d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/AssociationCollection.php @@ -0,0 +1,391 @@ +_tableLocator = $tableLocator; + } + } + + /** + * Add an association to the collection + * + * If the alias added contains a `.` the part preceding the `.` will be dropped. + * This makes using plugins simpler as the Plugin.Class syntax is frequently used. + * + * @param string $alias The association alias + * @param \Cake\ORM\Association $association The association to add. + * @return \Cake\ORM\Association The association object being added. + */ + public function add($alias, Association $association) + { + list(, $alias) = pluginSplit($alias); + + return $this->_items[strtolower($alias)] = $association; + } + + /** + * Creates and adds the Association object to this collection. + * + * @param string $className The name of association class. + * @param string $associated The alias for the target table. + * @param array $options List of options to configure the association definition. + * @return \Cake\ORM\Association + * @throws \InvalidArgumentException + */ + public function load($className, $associated, array $options = []) + { + $options += [ + 'tableLocator' => $this->getTableLocator() + ]; + + $association = new $className($associated, $options); + if (!$association instanceof Association) { + $message = sprintf('The association must extend `%s` class, `%s` given.', Association::class, get_class($association)); + throw new InvalidArgumentException($message); + } + + return $this->add($association->getName(), $association); + } + + /** + * Fetch an attached association by name. + * + * @param string $alias The association alias to get. + * @return \Cake\ORM\Association|null Either the association or null. + */ + public function get($alias) + { + $alias = strtolower($alias); + if (isset($this->_items[$alias])) { + return $this->_items[$alias]; + } + + return null; + } + + /** + * Fetch an association by property name. + * + * @param string $prop The property to find an association by. + * @return \Cake\ORM\Association|null Either the association or null. + */ + public function getByProperty($prop) + { + foreach ($this->_items as $assoc) { + if ($assoc->getProperty() === $prop) { + return $assoc; + } + } + + return null; + } + + /** + * Check for an attached association by name. + * + * @param string $alias The association alias to get. + * @return bool Whether or not the association exists. + */ + public function has($alias) + { + return isset($this->_items[strtolower($alias)]); + } + + /** + * Get the names of all the associations in the collection. + * + * @return array + */ + public function keys() + { + return array_keys($this->_items); + } + + /** + * Get an array of associations matching a specific type. + * + * @param string|array $class The type of associations you want. + * For example 'BelongsTo' or array like ['BelongsTo', 'HasOne'] + * @return array An array of Association objects. + * @deprecated 3.5.3 Use getByType() instead. + */ + public function type($class) + { + deprecationWarning( + 'AssociationCollection::type() is deprecated. ' . + 'Use getByType() instead.' + ); + + return $this->getByType($class); + } + + /** + * Get an array of associations matching a specific type. + * + * @param string|array $class The type of associations you want. + * For example 'BelongsTo' or array like ['BelongsTo', 'HasOne'] + * @return array An array of Association objects. + * @since 3.5.3 + */ + public function getByType($class) + { + $class = array_map('strtolower', (array)$class); + + $out = array_filter($this->_items, function ($assoc) use ($class) { + list(, $name) = namespaceSplit(get_class($assoc)); + + return in_array(strtolower($name), $class, true); + }); + + return array_values($out); + } + + /** + * Drop/remove an association. + * + * Once removed the association will not longer be reachable + * + * @param string $alias The alias name. + * @return void + */ + public function remove($alias) + { + unset($this->_items[strtolower($alias)]); + } + + /** + * Remove all registered associations. + * + * Once removed associations will not longer be reachable + * + * @return void + */ + public function removeAll() + { + foreach ($this->_items as $alias => $object) { + $this->remove($alias); + } + } + + /** + * Save all the associations that are parents of the given entity. + * + * Parent associations include any association where the given table + * is the owning side. + * + * @param \Cake\ORM\Table $table The table entity is for. + * @param \Cake\Datasource\EntityInterface $entity The entity to save associated data for. + * @param array $associations The list of associations to save parents from. + * associations not in this list will not be saved. + * @param array $options The options for the save operation. + * @return bool Success + */ + public function saveParents(Table $table, EntityInterface $entity, $associations, array $options = []) + { + if (empty($associations)) { + return true; + } + + return $this->_saveAssociations($table, $entity, $associations, $options, false); + } + + /** + * Save all the associations that are children of the given entity. + * + * Child associations include any association where the given table + * is not the owning side. + * + * @param \Cake\ORM\Table $table The table entity is for. + * @param \Cake\Datasource\EntityInterface $entity The entity to save associated data for. + * @param array $associations The list of associations to save children from. + * associations not in this list will not be saved. + * @param array $options The options for the save operation. + * @return bool Success + */ + public function saveChildren(Table $table, EntityInterface $entity, array $associations, array $options) + { + if (empty($associations)) { + return true; + } + + return $this->_saveAssociations($table, $entity, $associations, $options, true); + } + + /** + * Helper method for saving an association's data. + * + * @param \Cake\ORM\Table $table The table the save is currently operating on + * @param \Cake\Datasource\EntityInterface $entity The entity to save + * @param array $associations Array of associations to save. + * @param array $options Original options + * @param bool $owningSide Compared with association classes' + * isOwningSide method. + * @return bool Success + * @throws \InvalidArgumentException When an unknown alias is used. + */ + protected function _saveAssociations($table, $entity, $associations, $options, $owningSide) + { + unset($options['associated']); + foreach ($associations as $alias => $nested) { + if (is_int($alias)) { + $alias = $nested; + $nested = []; + } + $relation = $this->get($alias); + if (!$relation) { + $msg = sprintf( + 'Cannot save %s, it is not associated to %s', + $alias, + $table->getAlias() + ); + throw new InvalidArgumentException($msg); + } + if ($relation->isOwningSide($table) !== $owningSide) { + continue; + } + if (!$this->_save($relation, $entity, $nested, $options)) { + return false; + } + } + + return true; + } + + /** + * Helper method for saving an association's data. + * + * @param \Cake\ORM\Association $association The association object to save with. + * @param \Cake\Datasource\EntityInterface $entity The entity to save + * @param array $nested Options for deeper associations + * @param array $options Original options + * @return bool Success + */ + protected function _save($association, $entity, $nested, $options) + { + if (!$entity->isDirty($association->getProperty())) { + return true; + } + if (!empty($nested)) { + $options = (array)$nested + $options; + } + + return (bool)$association->saveAssociated($entity, $options); + } + + /** + * Cascade a delete across the various associations. + * Cascade first across associations for which cascadeCallbacks is true. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to delete associations for. + * @param array $options The options used in the delete operation. + * @return void + */ + public function cascadeDelete(EntityInterface $entity, array $options) + { + $noCascade = $this->_getNoCascadeItems($entity, $options); + foreach ($noCascade as $assoc) { + $assoc->cascadeDelete($entity, $options); + } + } + + /** + * Returns items that have no cascade callback. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to delete associations for. + * @param array $options The options used in the delete operation. + * @return \Cake\ORM\Association[] + */ + protected function _getNoCascadeItems($entity, $options) + { + $noCascade = []; + foreach ($this->_items as $assoc) { + if (!$assoc->getCascadeCallbacks()) { + $noCascade[] = $assoc; + continue; + } + $assoc->cascadeDelete($entity, $options); + } + + return $noCascade; + } + + /** + * Returns an associative array of association names out a mixed + * array. If true is passed, then it returns all association names + * in this collection. + * + * @param bool|array $keys the list of association names to normalize + * @return array + */ + public function normalizeKeys($keys) + { + if ($keys === true) { + $keys = $this->keys(); + } + + if (empty($keys)) { + return []; + } + + return $this->_normalizeAssociations($keys); + } + + /** + * Allow looping through the associations + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->_items); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/AssociationsNormalizerTrait.php b/app/vendor/cakephp/cakephp/src/ORM/AssociationsNormalizerTrait.php new file mode 100644 index 000000000..335804b41 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/AssociationsNormalizerTrait.php @@ -0,0 +1,67 @@ + $options) { + $pointer =& $result; + + if (is_int($table)) { + $table = $options; + $options = []; + } + + if (!strpos($table, '.')) { + $result[$table] = $options; + continue; + } + + $path = explode('.', $table); + $table = array_pop($path); + $first = array_shift($path); + $pointer += [$first => []]; + $pointer =& $pointer[$first]; + $pointer += ['associated' => []]; + + foreach ($path as $t) { + $pointer += ['associated' => []]; + $pointer['associated'] += [$t => []]; + $pointer['associated'][$t] += ['associated' => []]; + $pointer =& $pointer['associated'][$t]; + } + + $pointer['associated'] += [$table => []]; + $pointer['associated'][$table] = $options + $pointer['associated'][$table]; + } + + return isset($result['associated']) ? $result['associated'] : $result; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior.php new file mode 100644 index 000000000..ab11b6749 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior.php @@ -0,0 +1,422 @@ +doSomething($arg1, $arg2);`. + * + * ### Callback methods + * + * Behaviors can listen to any events fired on a Table. By default + * CakePHP provides a number of lifecycle events your behaviors can + * listen to: + * + * - `beforeFind(Event $event, Query $query, ArrayObject $options, boolean $primary)` + * Fired before each find operation. By stopping the event and supplying a + * return value you can bypass the find operation entirely. Any changes done + * to the $query instance will be retained for the rest of the find. The + * $primary parameter indicates whether or not this is the root query, + * or an associated query. + * + * - `buildValidator(Event $event, Validator $validator, string $name)` + * Fired when the validator object identified by $name is being built. You can use this + * callback to add validation rules or add validation providers. + * + * - `buildRules(Event $event, RulesChecker $rules)` + * Fired when the rules checking object for the table is being built. You can use this + * callback to add more rules to the set. + * + * - `beforeRules(Event $event, EntityInterface $entity, ArrayObject $options, $operation)` + * Fired before an entity is validated using by a rules checker. By stopping this event, + * you can return the final value of the rules checking operation. + * + * - `afterRules(Event $event, EntityInterface $entity, ArrayObject $options, bool $result, $operation)` + * Fired after the rules have been checked on the entity. By stopping this event, + * you can return the final value of the rules checking operation. + * + * - `beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)` + * Fired before each entity is saved. Stopping this event will abort the save + * operation. When the event is stopped the result of the event will be returned. + * + * - `afterSave(Event $event, EntityInterface $entity, ArrayObject $options)` + * Fired after an entity is saved. + * + * - `beforeDelete(Event $event, EntityInterface $entity, ArrayObject $options)` + * Fired before an entity is deleted. By stopping this event you will abort + * the delete operation. + * + * - `afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)` + * Fired after an entity has been deleted. + * + * In addition to the core events, behaviors can respond to any + * event fired from your Table classes including custom application + * specific ones. + * + * You can set the priority of a behaviors callbacks by using the + * `priority` setting when attaching a behavior. This will set the + * priority for all the callbacks a behavior provides. + * + * ### Finder methods + * + * Behaviors can provide finder methods that hook into a Table's + * find() method. Custom finders are a great way to provide preset + * queries that relate to your behavior. For example a SluggableBehavior + * could provide a find('slugged') finder. Behavior finders + * are implemented the same as other finders. Any method + * starting with `find` will be setup as a finder. Your finder + * methods should expect the following arguments: + * + * ``` + * findSlugged(Query $query, array $options) + * ``` + * + * @see \Cake\ORM\Table::addBehavior() + * @see \Cake\Event\EventManager + * @mixin \Cake\Core\InstanceConfigTrait + */ +class Behavior implements EventListenerInterface +{ + + use InstanceConfigTrait; + + /** + * Table instance. + * + * @var \Cake\ORM\Table + */ + protected $_table; + + /** + * Reflection method cache for behaviors. + * + * Stores the reflected method + finder methods per class. + * This prevents reflecting the same class multiple times in a single process. + * + * @var array + */ + protected static $_reflectionCache = []; + + /** + * Default configuration + * + * These are merged with user-provided configuration when the behavior is used. + * + * @var array + */ + protected $_defaultConfig = []; + + /** + * Constructor + * + * Merges config with the default and store in the config property + * + * @param \Cake\ORM\Table $table The table this behavior is attached to. + * @param array $config The config for this behavior. + */ + public function __construct(Table $table, array $config = []) + { + $config = $this->_resolveMethodAliases( + 'implementedFinders', + $this->_defaultConfig, + $config + ); + $config = $this->_resolveMethodAliases( + 'implementedMethods', + $this->_defaultConfig, + $config + ); + $this->_table = $table; + $this->setConfig($config); + $this->initialize($config); + } + + /** + * Constructor hook method. + * + * Implement this method to avoid having to overwrite + * the constructor and call parent. + * + * @param array $config The configuration settings provided to this behavior. + * @return void + */ + public function initialize(array $config) + { + } + + /** + * Get the table instance this behavior is bound to. + * + * @return \Cake\ORM\Table The bound table instance. + */ + public function getTable() + { + return $this->_table; + } + + /** + * Removes aliased methods that would otherwise be duplicated by userland configuration. + * + * @param string $key The key to filter. + * @param array $defaults The default method mappings. + * @param array $config The customized method mappings. + * @return array A de-duped list of config data. + */ + protected function _resolveMethodAliases($key, $defaults, $config) + { + if (!isset($defaults[$key], $config[$key])) { + return $config; + } + if (isset($config[$key]) && $config[$key] === []) { + $this->setConfig($key, [], false); + unset($config[$key]); + + return $config; + } + + $indexed = array_flip($defaults[$key]); + $indexedCustom = array_flip($config[$key]); + foreach ($indexed as $method => $alias) { + if (!isset($indexedCustom[$method])) { + $indexedCustom[$method] = $alias; + } + } + $this->setConfig($key, array_flip($indexedCustom), false); + unset($config[$key]); + + return $config; + } + + /** + * verifyConfig + * + * Checks that implemented keys contain values pointing at callable. + * + * @return void + * @throws \Cake\Core\Exception\Exception if config are invalid + */ + public function verifyConfig() + { + $keys = ['implementedFinders', 'implementedMethods']; + foreach ($keys as $key) { + if (!isset($this->_config[$key])) { + continue; + } + + foreach ($this->_config[$key] as $method) { + if (!is_callable([$this, $method])) { + throw new Exception(sprintf('The method %s is not callable on class %s', $method, get_class($this))); + } + } + } + } + + /** + * Gets the Model callbacks this behavior is interested in. + * + * By defining one of the callback methods a behavior is assumed + * to be interested in the related event. + * + * Override this method if you need to add non-conventional event listeners. + * Or if you want your behavior to listen to non-standard events. + * + * @return array + */ + public function implementedEvents() + { + $eventMap = [ + 'Model.beforeMarshal' => 'beforeMarshal', + 'Model.beforeFind' => 'beforeFind', + 'Model.beforeSave' => 'beforeSave', + 'Model.afterSave' => 'afterSave', + 'Model.afterSaveCommit' => 'afterSaveCommit', + 'Model.beforeDelete' => 'beforeDelete', + 'Model.afterDelete' => 'afterDelete', + 'Model.afterDeleteCommit' => 'afterDeleteCommit', + 'Model.buildValidator' => 'buildValidator', + 'Model.buildRules' => 'buildRules', + 'Model.beforeRules' => 'beforeRules', + 'Model.afterRules' => 'afterRules', + ]; + $config = $this->getConfig(); + $priority = isset($config['priority']) ? $config['priority'] : null; + $events = []; + + foreach ($eventMap as $event => $method) { + if (!method_exists($this, $method)) { + continue; + } + if ($priority === null) { + $events[$event] = $method; + } else { + $events[$event] = [ + 'callable' => $method, + 'priority' => $priority + ]; + } + } + + return $events; + } + + /** + * implementedFinders + * + * Provides an alias->methodname map of which finders a behavior implements. Example: + * + * ``` + * [ + * 'this' => 'findThis', + * 'alias' => 'findMethodName' + * ] + * ``` + * + * With the above example, a call to `$Table->find('this')` will call `$Behavior->findThis()` + * and a call to `$Table->find('alias')` will call `$Behavior->findMethodName()` + * + * It is recommended, though not required, to define implementedFinders in the config property + * of child classes such that it is not necessary to use reflections to derive the available + * method list. See core behaviors for examples + * + * @return array + * @throws \ReflectionException + */ + public function implementedFinders() + { + $methods = $this->getConfig('implementedFinders'); + if (isset($methods)) { + return $methods; + } + + return $this->_reflectionCache()['finders']; + } + + /** + * implementedMethods + * + * Provides an alias->methodname map of which methods a behavior implements. Example: + * + * ``` + * [ + * 'method' => 'method', + * 'aliasedmethod' => 'somethingElse' + * ] + * ``` + * + * With the above example, a call to `$Table->method()` will call `$Behavior->method()` + * and a call to `$Table->aliasedmethod()` will call `$Behavior->somethingElse()` + * + * It is recommended, though not required, to define implementedFinders in the config property + * of child classes such that it is not necessary to use reflections to derive the available + * method list. See core behaviors for examples + * + * @return array + * @throws \ReflectionException + */ + public function implementedMethods() + { + $methods = $this->getConfig('implementedMethods'); + if (isset($methods)) { + return $methods; + } + + return $this->_reflectionCache()['methods']; + } + + /** + * Gets the methods implemented by this behavior + * + * Uses the implementedEvents() method to exclude callback methods. + * Methods starting with `_` will be ignored, as will methods + * declared on Cake\ORM\Behavior + * + * @return array + * @throws \ReflectionException + */ + protected function _reflectionCache() + { + $class = get_class($this); + if (isset(self::$_reflectionCache[$class])) { + return self::$_reflectionCache[$class]; + } + + $events = $this->implementedEvents(); + $eventMethods = []; + foreach ($events as $e => $binding) { + if (is_array($binding) && isset($binding['callable'])) { + /* @var string $callable */ + $callable = $binding['callable']; + $binding = $callable; + } + $eventMethods[$binding] = true; + } + + $baseClass = 'Cake\ORM\Behavior'; + if (isset(self::$_reflectionCache[$baseClass])) { + $baseMethods = self::$_reflectionCache[$baseClass]; + } else { + $baseMethods = get_class_methods($baseClass); + self::$_reflectionCache[$baseClass] = $baseMethods; + } + + $return = [ + 'finders' => [], + 'methods' => [] + ]; + + $reflection = new ReflectionClass($class); + + foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + $methodName = $method->getName(); + if (in_array($methodName, $baseMethods) || + isset($eventMethods[$methodName]) + ) { + continue; + } + + if (substr($methodName, 0, 4) === 'find') { + $return['finders'][lcfirst(substr($methodName, 4))] = $methodName; + } else { + $return['methods'][$methodName] = $methodName; + } + } + + return self::$_reflectionCache[$class] = $return; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/CounterCacheBehavior.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/CounterCacheBehavior.php new file mode 100644 index 000000000..b89a42d37 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/CounterCacheBehavior.php @@ -0,0 +1,287 @@ + [ + * 'post_count' + * ] + * ] + * ``` + * + * Counter cache with scope + * ``` + * [ + * 'Users' => [ + * 'posts_published' => [ + * 'conditions' => [ + * 'published' => true + * ] + * ] + * ] + * ] + * ``` + * + * Counter cache using custom find + * ``` + * [ + * 'Users' => [ + * 'posts_published' => [ + * 'finder' => 'published' // Will be using findPublished() + * ] + * ] + * ] + * ``` + * + * Counter cache using lambda function returning the count + * This is equivalent to example #2 + * + * ``` + * [ + * 'Users' => [ + * 'posts_published' => function (Event $event, EntityInterface $entity, Table $table) { + * $query = $table->find('all')->where([ + * 'published' => true, + * 'user_id' => $entity->get('user_id') + * ]); + * return $query->count(); + * } + * ] + * ] + * ``` + * + * When using a lambda function you can return `false` to disable updating the counter value + * for the current operation. + * + * Ignore updating the field if it is dirty + * ``` + * [ + * 'Users' => [ + * 'posts_published' => [ + * 'ignoreDirty' => true + * ] + * ] + * ] + * ``` + * + * You can disable counter updates entirely by sending the `ignoreCounterCache` option + * to your save operation: + * + * ``` + * $this->Articles->save($article, ['ignoreCounterCache' => true]); + * ``` + */ +class CounterCacheBehavior extends Behavior +{ + + /** + * Store the fields which should be ignored + * + * @var array + */ + protected $_ignoreDirty = []; + + /** + * beforeSave callback. + * + * Check if a field, which should be ignored, is dirty + * + * @param \Cake\Event\Event $event The beforeSave event that was fired + * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved + * @param \ArrayObject $options The options for the query + * @return void + */ + public function beforeSave(Event $event, EntityInterface $entity, $options) + { + if (isset($options['ignoreCounterCache']) && $options['ignoreCounterCache'] === true) { + return; + } + + foreach ($this->_config as $assoc => $settings) { + $assoc = $this->_table->getAssociation($assoc); + foreach ($settings as $field => $config) { + if (is_int($field)) { + continue; + } + + $registryAlias = $assoc->getTarget()->getRegistryAlias(); + $entityAlias = $assoc->getProperty(); + + if (!is_callable($config) && + isset($config['ignoreDirty']) && + $config['ignoreDirty'] === true && + $entity->$entityAlias->isDirty($field) + ) { + $this->_ignoreDirty[$registryAlias][$field] = true; + } + } + } + } + + /** + * afterSave callback. + * + * Makes sure to update counter cache when a new record is created or updated. + * + * @param \Cake\Event\Event $event The afterSave event that was fired. + * @param \Cake\Datasource\EntityInterface $entity The entity that was saved. + * @param \ArrayObject $options The options for the query + * @return void + */ + public function afterSave(Event $event, EntityInterface $entity, $options) + { + if (isset($options['ignoreCounterCache']) && $options['ignoreCounterCache'] === true) { + return; + } + + $this->_processAssociations($event, $entity); + $this->_ignoreDirty = []; + } + + /** + * afterDelete callback. + * + * Makes sure to update counter cache when a record is deleted. + * + * @param \Cake\Event\Event $event The afterDelete event that was fired. + * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted. + * @param \ArrayObject $options The options for the query + * @return void + */ + public function afterDelete(Event $event, EntityInterface $entity, $options) + { + if (isset($options['ignoreCounterCache']) && $options['ignoreCounterCache'] === true) { + return; + } + + $this->_processAssociations($event, $entity); + } + + /** + * Iterate all associations and update counter caches. + * + * @param \Cake\Event\Event $event Event instance. + * @param \Cake\Datasource\EntityInterface $entity Entity. + * @return void + */ + protected function _processAssociations(Event $event, EntityInterface $entity) + { + foreach ($this->_config as $assoc => $settings) { + $assoc = $this->_table->getAssociation($assoc); + $this->_processAssociation($event, $entity, $assoc, $settings); + } + } + + /** + * Updates counter cache for a single association + * + * @param \Cake\Event\Event $event Event instance. + * @param \Cake\Datasource\EntityInterface $entity Entity + * @param \Cake\ORM\Association $assoc The association object + * @param array $settings The settings for for counter cache for this association + * @return void + * @throws \RuntimeException If invalid callable is passed. + */ + protected function _processAssociation(Event $event, EntityInterface $entity, Association $assoc, array $settings) + { + $foreignKeys = (array)$assoc->getForeignKey(); + $primaryKeys = (array)$assoc->getBindingKey(); + $countConditions = $entity->extract($foreignKeys); + $updateConditions = array_combine($primaryKeys, $countConditions); + $countOriginalConditions = $entity->extractOriginalChanged($foreignKeys); + + if ($countOriginalConditions !== []) { + $updateOriginalConditions = array_combine($primaryKeys, $countOriginalConditions); + } + + foreach ($settings as $field => $config) { + if (is_int($field)) { + $field = $config; + $config = []; + } + + if (isset($this->_ignoreDirty[$assoc->getTarget()->getRegistryAlias()][$field]) && + $this->_ignoreDirty[$assoc->getTarget()->getRegistryAlias()][$field] === true + ) { + continue; + } + + if (is_callable($config)) { + if (is_string($config)) { + throw new RuntimeException('You must not use a string as callable.'); + } + $count = $config($event, $entity, $this->_table, false); + } else { + $count = $this->_getCount($config, $countConditions); + } + if ($count !== false) { + $assoc->getTarget()->updateAll([$field => $count], $updateConditions); + } + + if (isset($updateOriginalConditions)) { + if (is_callable($config)) { + if (is_string($config)) { + throw new RuntimeException('You must not use a string as callable.'); + } + $count = $config($event, $entity, $this->_table, true); + } else { + $count = $this->_getCount($config, $countOriginalConditions); + } + if ($count !== false) { + $assoc->getTarget()->updateAll([$field => $count], $updateOriginalConditions); + } + } + } + } + + /** + * Fetches and returns the count for a single field in an association + * + * @param array $config The counter cache configuration for a single field + * @param array $conditions Additional conditions given to the query + * @return int The number of relations matching the given config and conditions + */ + protected function _getCount(array $config, array $conditions) + { + $finder = 'all'; + if (!empty($config['finder'])) { + $finder = $config['finder']; + unset($config['finder']); + } + + if (!isset($config['conditions'])) { + $config['conditions'] = []; + } + $config['conditions'] = array_merge($conditions, $config['conditions']); + $query = $this->_table->find($finder, $config); + + return $query->count(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/TimestampBehavior.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TimestampBehavior.php new file mode 100644 index 000000000..68e24af5f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TimestampBehavior.php @@ -0,0 +1,222 @@ + [], + 'implementedMethods' => [ + 'timestamp' => 'timestamp', + 'touch' => 'touch' + ], + 'events' => [ + 'Model.beforeSave' => [ + 'created' => 'new', + 'modified' => 'always' + ] + ], + 'refreshTimestamp' => true + ]; + + /** + * Current timestamp + * + * @var \DateTime + */ + protected $_ts; + + /** + * Initialize hook + * + * If events are specified - do *not* merge them with existing events, + * overwrite the events to listen on + * + * @param array $config The config for this behavior. + * @return void + */ + public function initialize(array $config) + { + if (isset($config['events'])) { + $this->setConfig('events', $config['events'], false); + } + } + + /** + * There is only one event handler, it can be configured to be called for any event + * + * @param \Cake\Event\Event $event Event instance. + * @param \Cake\Datasource\EntityInterface $entity Entity instance. + * @throws \UnexpectedValueException if a field's when value is misdefined + * @return bool Returns true irrespective of the behavior logic, the save will not be prevented. + * @throws \UnexpectedValueException When the value for an event is not 'always', 'new' or 'existing' + */ + public function handleEvent(Event $event, EntityInterface $entity) + { + $eventName = $event->getName(); + $events = $this->_config['events']; + + $new = $entity->isNew() !== false; + $refresh = $this->_config['refreshTimestamp']; + + foreach ($events[$eventName] as $field => $when) { + if (!in_array($when, ['always', 'new', 'existing'])) { + throw new UnexpectedValueException( + sprintf('When should be one of "always", "new" or "existing". The passed value "%s" is invalid', $when) + ); + } + if ($when === 'always' || + ($when === 'new' && $new) || + ($when === 'existing' && !$new) + ) { + $this->_updateField($entity, $field, $refresh); + } + } + + return true; + } + + /** + * implementedEvents + * + * The implemented events of this behavior depend on configuration + * + * @return array + */ + public function implementedEvents() + { + return array_fill_keys(array_keys($this->_config['events']), 'handleEvent'); + } + + /** + * Get or set the timestamp to be used + * + * Set the timestamp to the given DateTime object, or if not passed a new DateTime object + * If an explicit date time is passed, the config option `refreshTimestamp` is + * automatically set to false. + * + * @param \DateTime|null $ts Timestamp + * @param bool $refreshTimestamp If true timestamp is refreshed. + * @return \DateTime + */ + public function timestamp(DateTime $ts = null, $refreshTimestamp = false) + { + if ($ts) { + if ($this->_config['refreshTimestamp']) { + $this->_config['refreshTimestamp'] = false; + } + $this->_ts = new Time($ts); + } elseif ($this->_ts === null || $refreshTimestamp) { + $this->_ts = new Time(); + } + + return $this->_ts; + } + + /** + * Touch an entity + * + * Bumps timestamp fields for an entity. For any fields configured to be updated + * "always" or "existing", update the timestamp value. This method will overwrite + * any pre-existing value. + * + * @param \Cake\Datasource\EntityInterface $entity Entity instance. + * @param string $eventName Event name. + * @return bool true if a field is updated, false if no action performed + */ + public function touch(EntityInterface $entity, $eventName = 'Model.beforeSave') + { + $events = $this->_config['events']; + if (empty($events[$eventName])) { + return false; + } + + $return = false; + $refresh = $this->_config['refreshTimestamp']; + + foreach ($events[$eventName] as $field => $when) { + if (in_array($when, ['always', 'existing'])) { + $return = true; + $entity->setDirty($field, false); + $this->_updateField($entity, $field, $refresh); + } + } + + return $return; + } + + /** + * Update a field, if it hasn't been updated already + * + * @param \Cake\Datasource\EntityInterface $entity Entity instance. + * @param string $field Field name + * @param bool $refreshTimestamp Whether to refresh timestamp. + * @return void + */ + protected function _updateField($entity, $field, $refreshTimestamp) + { + if ($entity->isDirty($field)) { + return; + } + + $ts = $this->timestamp(null, $refreshTimestamp); + + $columnType = $this->getTable()->getSchema()->getColumnType($field); + if (!$columnType) { + return; + } + + /** @var \Cake\Database\Type\DateTimeType $type */ + $type = Type::build($columnType); + + if (!$type instanceof Type\DateTimeType) { + deprecationWarning('TimestampBehavior support for column types other than DateTimeType will be removed in 4.0.'); + $entity->set($field, (string)$ts); + + return; + } + + $class = $type->getDateTimeClassName(); + + $entity->set($field, new $class($ts)); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/TranslateTrait.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/TranslateTrait.php new file mode 100644 index 000000000..343740e23 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/TranslateTrait.php @@ -0,0 +1,65 @@ +get('_locale')) { + return $this; + } + + $i18n = $this->get('_translations'); + $created = false; + + if (empty($i18n)) { + $i18n = []; + $created = true; + } + + if ($created || empty($i18n[$language]) || !($i18n[$language] instanceof EntityInterface)) { + $className = get_class($this); + + $i18n[$language] = new $className(); + $created = true; + } + + if ($created) { + $this->set('_translations', $i18n); + } + + // Assume the user will modify any of the internal translations, helps with saving + $this->setDirty('_translations', true); + + return $i18n[$language]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/TranslateBehavior.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TranslateBehavior.php new file mode 100644 index 000000000..77f9dcce4 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TranslateBehavior.php @@ -0,0 +1,768 @@ + ['translations' => 'findTranslations'], + 'implementedMethods' => [ + 'setLocale' => 'setLocale', + 'getLocale' => 'getLocale', + 'locale' => 'locale', + 'translationField' => 'translationField' + ], + 'fields' => [], + 'translationTable' => 'I18n', + 'defaultLocale' => '', + 'referenceName' => '', + 'allowEmptyTranslations' => true, + 'onlyTranslated' => false, + 'strategy' => 'subquery', + 'tableLocator' => null, + 'validator' => false + ]; + + /** + * Constructor + * + * @param \Cake\ORM\Table $table The table this behavior is attached to. + * @param array $config The config for this behavior. + */ + public function __construct(Table $table, array $config = []) + { + $config += [ + 'defaultLocale' => I18n::getDefaultLocale(), + 'referenceName' => $this->_referenceName($table) + ]; + + if (isset($config['tableLocator'])) { + $this->_tableLocator = $config['tableLocator']; + } else { + $this->_tableLocator = $table->associations()->getTableLocator(); + } + + parent::__construct($table, $config); + } + + /** + * Initialize hook + * + * @param array $config The config for this behavior. + * @return void + */ + public function initialize(array $config) + { + $this->_translationTable = $this->getTableLocator()->get($this->_config['translationTable']); + + $this->setupFieldAssociations( + $this->_config['fields'], + $this->_config['translationTable'], + $this->_config['referenceName'], + $this->_config['strategy'] + ); + } + + /** + * Creates the associations between the bound table and every field passed to + * this method. + * + * Additionally it creates a `i18n` HasMany association that will be + * used for fetching all translations for each record in the bound table + * + * @param array $fields list of fields to create associations for + * @param string $table the table name to use for storing each field translation + * @param string $model the model field value + * @param string $strategy the strategy used in the _i18n association + * + * @return void + */ + public function setupFieldAssociations($fields, $table, $model, $strategy) + { + $targetAlias = $this->_translationTable->getAlias(); + $alias = $this->_table->getAlias(); + $filter = $this->_config['onlyTranslated']; + $tableLocator = $this->getTableLocator(); + + foreach ($fields as $field) { + $name = $alias . '_' . $field . '_translation'; + + if (!$tableLocator->exists($name)) { + $fieldTable = $tableLocator->get($name, [ + 'className' => $table, + 'alias' => $name, + 'table' => $this->_translationTable->getTable() + ]); + } else { + $fieldTable = $tableLocator->get($name); + } + + $conditions = [ + $name . '.model' => $model, + $name . '.field' => $field, + ]; + if (!$this->_config['allowEmptyTranslations']) { + $conditions[$name . '.content !='] = ''; + } + + $this->_table->hasOne($name, [ + 'targetTable' => $fieldTable, + 'foreignKey' => 'foreign_key', + 'joinType' => $filter ? QueryInterface::JOIN_TYPE_INNER : QueryInterface::JOIN_TYPE_LEFT, + 'conditions' => $conditions, + 'propertyName' => $field . '_translation' + ]); + } + + $conditions = ["$targetAlias.model" => $model]; + if (!$this->_config['allowEmptyTranslations']) { + $conditions["$targetAlias.content !="] = ''; + } + + $this->_table->hasMany($targetAlias, [ + 'className' => $table, + 'foreignKey' => 'foreign_key', + 'strategy' => $strategy, + 'conditions' => $conditions, + 'propertyName' => '_i18n', + 'dependent' => true + ]); + } + + /** + * Callback method that listens to the `beforeFind` event in the bound + * table. It modifies the passed query by eager loading the translated fields + * and adding a formatter to copy the values into the main table records. + * + * @param \Cake\Event\Event $event The beforeFind event that was fired. + * @param \Cake\ORM\Query $query Query + * @param \ArrayObject $options The options for the query + * @return void + */ + public function beforeFind(Event $event, Query $query, $options) + { + $locale = $this->getLocale(); + + if ($locale === $this->getConfig('defaultLocale')) { + return; + } + + $conditions = function ($field, $locale, $query, $select) { + return function ($q) use ($field, $locale, $query, $select) { + /* @var \Cake\Datasource\QueryInterface $q */ + $q->where([$q->getRepository()->aliasField('locale') => $locale]); + + /* @var \Cake\ORM\Query $query */ + if ($query->isAutoFieldsEnabled() || + in_array($field, $select, true) || + in_array($this->_table->aliasField($field), $select, true) + ) { + $q->select(['id', 'content']); + } + + return $q; + }; + }; + + $contain = []; + $fields = $this->_config['fields']; + $alias = $this->_table->getAlias(); + $select = $query->clause('select'); + + $changeFilter = isset($options['filterByCurrentLocale']) && + $options['filterByCurrentLocale'] !== $this->_config['onlyTranslated']; + + foreach ($fields as $field) { + $name = $alias . '_' . $field . '_translation'; + + $contain[$name]['queryBuilder'] = $conditions( + $field, + $locale, + $query, + $select + ); + + if ($changeFilter) { + $filter = $options['filterByCurrentLocale'] ? QueryInterface::JOIN_TYPE_INNER : QueryInterface::JOIN_TYPE_LEFT; + $contain[$name]['joinType'] = $filter; + } + } + + $query->contain($contain); + $query->formatResults(function ($results) use ($locale) { + return $this->_rowMapper($results, $locale); + }, $query::PREPEND); + } + + /** + * Modifies the entity before it is saved so that translated fields are persisted + * in the database too. + * + * @param \Cake\Event\Event $event The beforeSave event that was fired + * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved + * @param \ArrayObject $options the options passed to the save method + * @return void + */ + public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options) + { + $locale = $entity->get('_locale') ?: $this->getLocale(); + $newOptions = [$this->_translationTable->getAlias() => ['validate' => false]]; + $options['associated'] = $newOptions + $options['associated']; + + // Check early if empty translations are present in the entity. + // If this is the case, unset them to prevent persistence. + // This only applies if $this->_config['allowEmptyTranslations'] is false + if ($this->_config['allowEmptyTranslations'] === false) { + $this->_unsetEmptyFields($entity); + } + + $this->_bundleTranslatedFields($entity); + $bundled = $entity->get('_i18n') ?: []; + $noBundled = count($bundled) === 0; + + // No additional translation records need to be saved, + // as the entity is in the default locale. + if ($noBundled && $locale === $this->getConfig('defaultLocale')) { + return; + } + + $values = $entity->extract($this->_config['fields'], true); + $fields = array_keys($values); + $noFields = empty($fields); + + // If there are no fields and no bundled translations, or both fields + // in the default locale and bundled translations we can + // skip the remaining logic as its not necessary. + if ($noFields && $noBundled || ($fields && $bundled)) { + return; + } + + $primaryKey = (array)$this->_table->getPrimaryKey(); + $key = $entity->get(current($primaryKey)); + + // When we have no key and bundled translations, we + // need to mark the entity dirty so the root + // entity persists. + if ($noFields && $bundled && !$key) { + foreach ($this->_config['fields'] as $field) { + $entity->setDirty($field, true); + } + + return; + } + + if ($noFields) { + return; + } + + $model = $this->_config['referenceName']; + $preexistent = $this->_translationTable->find() + ->select(['id', 'field']) + ->where([ + 'field IN' => $fields, + 'locale' => $locale, + 'foreign_key' => $key, + 'model' => $model + ]) + ->enableBufferedResults(false) + ->all() + ->indexBy('field'); + + $modified = []; + foreach ($preexistent as $field => $translation) { + $translation->set('content', $values[$field]); + $modified[$field] = $translation; + } + + $new = array_diff_key($values, $modified); + foreach ($new as $field => $content) { + $new[$field] = new Entity(compact('locale', 'field', 'content', 'model'), [ + 'useSetters' => false, + 'markNew' => true + ]); + } + + $entity->set('_i18n', array_merge($bundled, array_values($modified + $new))); + $entity->set('_locale', $locale, ['setter' => false]); + $entity->setDirty('_locale', false); + + foreach ($fields as $field) { + $entity->setDirty($field, false); + } + } + + /** + * Unsets the temporary `_i18n` property after the entity has been saved + * + * @param \Cake\Event\Event $event The beforeSave event that was fired + * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved + * @return void + */ + public function afterSave(Event $event, EntityInterface $entity) + { + $entity->unsetProperty('_i18n'); + } + + /** + * Add in `_translations` marshalling handlers. You can disable marshalling + * of translations by setting `'translations' => false` in the options + * provided to `Table::newEntity()` or `Table::patchEntity()`. + * + * {@inheritDoc} + */ + public function buildMarshalMap($marshaller, $map, $options) + { + if (isset($options['translations']) && !$options['translations']) { + return []; + } + + return [ + '_translations' => function ($value, $entity) use ($marshaller, $options) { + /* @var \Cake\Datasource\EntityInterface $entity */ + $translations = $entity->get('_translations'); + foreach ($this->_config['fields'] as $field) { + $options['validate'] = $this->_config['validator']; + $errors = []; + if (!is_array($value)) { + return null; + } + foreach ($value as $language => $fields) { + if (!isset($translations[$language])) { + $translations[$language] = $this->_table->newEntity(); + } + $marshaller->merge($translations[$language], $fields, $options); + if ((bool)$translations[$language]->getErrors()) { + $errors[$language] = $translations[$language]->getErrors(); + } + } + // Set errors into the root entity, so validation errors + // match the original form data position. + $entity->setErrors($errors); + } + + return $translations; + } + ]; + } + + /** + * Sets the locale that should be used for all future find and save operations on + * the table where this behavior is attached to. + * + * When fetching records, the behavior will include the content for the locale set + * via this method, and likewise when saving data, it will save the data in that + * locale. + * + * Note that in case an entity has a `_locale` property set, that locale will win + * over the locale set via this method (and over the globally configured one for + * that matter)! + * + * @param string|null $locale The locale to use for fetching and saving records. Pass `null` + * in order to unset the current locale, and to make the behavior fall back to using the + * globally configured locale. + * @return $this + * @see \Cake\ORM\Behavior\TranslateBehavior::getLocale() + * @link https://book.cakephp.org/3.0/en/orm/behaviors/translate.html#retrieving-one-language-without-using-i18n-locale + * @link https://book.cakephp.org/3.0/en/orm/behaviors/translate.html#saving-in-another-language + */ + public function setLocale($locale) + { + $this->_locale = $locale; + + return $this; + } + + /** + * Returns the current locale. + * + * If no locale has been explicitly set via `setLocale()`, this method will return + * the currently configured global locale. + * + * @return string + * @see \Cake\I18n\I18n::getLocale() + * @see \Cake\ORM\Behavior\TranslateBehavior::setLocale() + */ + public function getLocale() + { + return $this->_locale ?: I18n::getLocale(); + } + + /** + * Sets all future finds for the bound table to also fetch translated fields for + * the passed locale. If no value is passed, it returns the currently configured + * locale + * + * @deprecated 3.6.0 Use setLocale()/getLocale() instead. + * @param string|null $locale The locale to use for fetching translated records + * @return string + */ + public function locale($locale = null) + { + deprecationWarning( + get_called_class() . '::locale() is deprecated. ' . + 'Use setLocale()/getLocale() instead.' + ); + + if ($locale !== null) { + $this->setLocale($locale); + } + + return $this->getLocale(); + } + + /** + * Returns a fully aliased field name for translated fields. + * + * If the requested field is configured as a translation field, the `content` + * field with an alias of a corresponding association is returned. Table-aliased + * field name is returned for all other fields. + * + * @param string $field Field name to be aliased. + * @return string + */ + public function translationField($field) + { + $table = $this->_table; + if ($this->getLocale() === $this->getConfig('defaultLocale')) { + return $table->aliasField($field); + } + $associationName = $table->getAlias() . '_' . $field . '_translation'; + + if ($table->associations()->has($associationName)) { + return $associationName . '.content'; + } + + return $table->aliasField($field); + } + + /** + * Custom finder method used to retrieve all translations for the found records. + * Fetched translations can be filtered by locale by passing the `locales` key + * in the options array. + * + * Translated values will be found for each entity under the property `_translations`, + * containing an array indexed by locale name. + * + * ### Example: + * + * ``` + * $article = $articles->find('translations', ['locales' => ['eng', 'deu'])->first(); + * $englishTranslatedFields = $article->get('_translations')['eng']; + * ``` + * + * If the `locales` array is not passed, it will bring all translations found + * for each record. + * + * @param \Cake\ORM\Query $query The original query to modify + * @param array $options Options + * @return \Cake\ORM\Query + */ + public function findTranslations(Query $query, array $options) + { + $locales = isset($options['locales']) ? $options['locales'] : []; + $targetAlias = $this->_translationTable->getAlias(); + + return $query + ->contain([$targetAlias => function ($query) use ($locales, $targetAlias) { + if ($locales) { + /* @var \Cake\Datasource\QueryInterface $query */ + $query->where(["$targetAlias.locale IN" => $locales]); + } + + return $query; + }]) + ->formatResults([$this, 'groupTranslations'], $query::PREPEND); + } + + /** + * Determine the reference name to use for a given table + * + * The reference name is usually derived from the class name of the table object + * (PostsTable -> Posts), however for autotable instances it is derived from + * the database table the object points at - or as a last resort, the alias + * of the autotable instance. + * + * @param \Cake\ORM\Table $table The table class to get a reference name for. + * @return string + */ + protected function _referenceName(Table $table) + { + $name = namespaceSplit(get_class($table)); + $name = substr(end($name), 0, -5); + if (empty($name)) { + $name = $table->getTable() ?: $table->getAlias(); + $name = Inflector::camelize($name); + } + + return $name; + } + + /** + * Modifies the results from a table find in order to merge the translated fields + * into each entity for a given locale. + * + * @param \Cake\Datasource\ResultSetInterface $results Results to map. + * @param string $locale Locale string + * @return \Cake\Collection\CollectionInterface + */ + protected function _rowMapper($results, $locale) + { + return $results->map(function ($row) use ($locale) { + if ($row === null) { + return $row; + } + $hydrated = !is_array($row); + + foreach ($this->_config['fields'] as $field) { + $name = $field . '_translation'; + $translation = isset($row[$name]) ? $row[$name] : null; + + if ($translation === null || $translation === false) { + unset($row[$name]); + continue; + } + + $content = isset($translation['content']) ? $translation['content'] : null; + if ($content !== null) { + $row[$field] = $content; + } + + unset($row[$name]); + } + + $row['_locale'] = $locale; + if ($hydrated) { + /* @var \Cake\Datasource\EntityInterface $row */ + $row->clean(); + } + + return $row; + }); + } + + /** + * Modifies the results from a table find in order to merge full translation records + * into each entity under the `_translations` key + * + * @param \Cake\Datasource\ResultSetInterface $results Results to modify. + * @return \Cake\Collection\CollectionInterface + */ + public function groupTranslations($results) + { + return $results->map(function ($row) { + if (!$row instanceof EntityInterface) { + return $row; + } + $translations = (array)$row->get('_i18n'); + if (empty($translations) && $row->get('_translations')) { + return $row; + } + $grouped = new Collection($translations); + + $result = []; + foreach ($grouped->combine('field', 'content', 'locale') as $locale => $keys) { + $entityClass = $this->_table->getEntityClass(); + $translation = new $entityClass($keys + ['locale' => $locale], [ + 'markNew' => false, + 'useSetters' => false, + 'markClean' => true + ]); + $result[$locale] = $translation; + } + + $options = ['setter' => false, 'guard' => false]; + $row->set('_translations', $result, $options); + unset($row['_i18n']); + $row->clean(); + + return $row; + }); + } + + /** + * Helper method used to generated multiple translated field entities + * out of the data found in the `_translations` property in the passed + * entity. The result will be put into its `_i18n` property + * + * @param \Cake\Datasource\EntityInterface $entity Entity + * @return void + */ + protected function _bundleTranslatedFields($entity) + { + $translations = (array)$entity->get('_translations'); + + if (empty($translations) && !$entity->isDirty('_translations')) { + return; + } + + $fields = $this->_config['fields']; + $primaryKey = (array)$this->_table->getPrimaryKey(); + $key = $entity->get(current($primaryKey)); + $find = []; + $contents = []; + + foreach ($translations as $lang => $translation) { + foreach ($fields as $field) { + if (!$translation->isDirty($field)) { + continue; + } + $find[] = ['locale' => $lang, 'field' => $field, 'foreign_key' => $key]; + $contents[] = new Entity(['content' => $translation->get($field)], [ + 'useSetters' => false + ]); + } + } + + if (empty($find)) { + return; + } + + $results = $this->_findExistingTranslations($find); + + foreach ($find as $i => $translation) { + if (!empty($results[$i])) { + $contents[$i]->set('id', $results[$i], ['setter' => false]); + $contents[$i]->isNew(false); + } else { + $translation['model'] = $this->_config['referenceName']; + $contents[$i]->set($translation, ['setter' => false, 'guard' => false]); + $contents[$i]->isNew(true); + } + } + + $entity->set('_i18n', $contents); + } + + /** + * Unset empty translations to avoid persistence. + * + * Should only be called if $this->_config['allowEmptyTranslations'] is false. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check for empty translations fields inside. + * @return void + */ + protected function _unsetEmptyFields(EntityInterface $entity) + { + $translations = (array)$entity->get('_translations'); + foreach ($translations as $locale => $translation) { + $fields = $translation->extract($this->_config['fields'], false); + foreach ($fields as $field => $value) { + if (strlen($value) === 0) { + $translation->unsetProperty($field); + } + } + + $translation = $translation->extract($this->_config['fields']); + + // If now, the current locale property is empty, + // unset it completely. + if (empty(array_filter($translation))) { + unset($entity->get('_translations')[$locale]); + } + } + + // If now, the whole _translations property is empty, + // unset it completely and return + if (empty($entity->get('_translations'))) { + $entity->unsetProperty('_translations'); + } + } + + /** + * Returns the ids found for each of the condition arrays passed for the translations + * table. Each records is indexed by the corresponding position to the conditions array + * + * @param array $ruleSet an array of arary of conditions to be used for finding each + * @return array + */ + protected function _findExistingTranslations($ruleSet) + { + $association = $this->_table->getAssociation($this->_translationTable->getAlias()); + + $query = $association->find() + ->select(['id', 'num' => 0]) + ->where(current($ruleSet)) + ->enableHydration(false) + ->enableBufferedResults(false); + + unset($ruleSet[0]); + foreach ($ruleSet as $i => $conditions) { + $q = $association->find() + ->select(['id', 'num' => $i]) + ->where($conditions); + $query->unionAll($q); + } + + return $query->all()->combine('num', 'id')->toArray(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/TreeBehavior.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TreeBehavior.php new file mode 100644 index 000000000..70714be2e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TreeBehavior.php @@ -0,0 +1,1016 @@ + [ + 'path' => 'findPath', + 'children' => 'findChildren', + 'treeList' => 'findTreeList', + ], + 'implementedMethods' => [ + 'childCount' => 'childCount', + 'moveUp' => 'moveUp', + 'moveDown' => 'moveDown', + 'recover' => 'recover', + 'removeFromTree' => 'removeFromTree', + 'getLevel' => 'getLevel', + 'formatTreeList' => 'formatTreeList', + ], + 'parent' => 'parent_id', + 'left' => 'lft', + 'right' => 'rght', + 'scope' => null, + 'level' => null, + 'recoverOrder' => null, + ]; + + /** + * {@inheritDoc} + */ + public function initialize(array $config) + { + $this->_config['leftField'] = new IdentifierExpression($this->_config['left']); + $this->_config['rightField'] = new IdentifierExpression($this->_config['right']); + } + + /** + * Before save listener. + * Transparently manages setting the lft and rght fields if the parent field is + * included in the parameters to be saved. + * + * @param \Cake\Event\Event $event The beforeSave event that was fired + * @param \Cake\Datasource\EntityInterface $entity the entity that is going to be saved + * @return void + * @throws \RuntimeException if the parent to set for the node is invalid + */ + public function beforeSave(Event $event, EntityInterface $entity) + { + $isNew = $entity->isNew(); + $config = $this->getConfig(); + $parent = $entity->get($config['parent']); + $primaryKey = $this->_getPrimaryKey(); + $dirty = $entity->isDirty($config['parent']); + $level = $config['level']; + + if ($parent && $entity->get($primaryKey) == $parent) { + throw new RuntimeException("Cannot set a node's parent as itself"); + } + + if ($isNew && $parent) { + $parentNode = $this->_getNode($parent); + $edge = $parentNode->get($config['right']); + $entity->set($config['left'], $edge); + $entity->set($config['right'], $edge + 1); + $this->_sync(2, '+', ">= {$edge}"); + + if ($level) { + $entity->set($level, $parentNode[$level] + 1); + } + + return; + } + + if ($isNew && !$parent) { + $edge = $this->_getMax(); + $entity->set($config['left'], $edge + 1); + $entity->set($config['right'], $edge + 2); + + if ($level) { + $entity->set($level, 0); + } + + return; + } + + if (!$isNew && $dirty && $parent) { + $this->_setParent($entity, $parent); + + if ($level) { + $parentNode = $this->_getNode($parent); + $entity->set($level, $parentNode[$level] + 1); + } + + return; + } + + if (!$isNew && $dirty && !$parent) { + $this->_setAsRoot($entity); + + if ($level) { + $entity->set($level, 0); + } + } + } + + /** + * After save listener. + * + * Manages updating level of descendants of currently saved entity. + * + * @param \Cake\Event\Event $event The afterSave event that was fired + * @param \Cake\Datasource\EntityInterface $entity the entity that is going to be saved + * @return void + */ + public function afterSave(Event $event, EntityInterface $entity) + { + if (!$this->_config['level'] || $entity->isNew()) { + return; + } + + $this->_setChildrenLevel($entity); + } + + /** + * Set level for descendants. + * + * @param \Cake\Datasource\EntityInterface $entity The entity whose descendants need to be updated. + * @return void + */ + protected function _setChildrenLevel($entity) + { + $config = $this->getConfig(); + + if ($entity->get($config['left']) + 1 === $entity->get($config['right'])) { + return; + } + + $primaryKey = $this->_getPrimaryKey(); + $primaryKeyValue = $entity->get($primaryKey); + $depths = [$primaryKeyValue => $entity->get($config['level'])]; + + $children = $this->_table->find('children', [ + 'for' => $primaryKeyValue, + 'fields' => [$this->_getPrimaryKey(), $config['parent'], $config['level']], + 'order' => $config['left'], + ]); + + /* @var \Cake\Datasource\EntityInterface $node */ + foreach ($children as $node) { + $parentIdValue = $node->get($config['parent']); + $depth = $depths[$parentIdValue] + 1; + $depths[$node->get($primaryKey)] = $depth; + + $this->_table->updateAll( + [$config['level'] => $depth], + [$primaryKey => $node->get($primaryKey)] + ); + } + } + + /** + * Also deletes the nodes in the subtree of the entity to be delete + * + * @param \Cake\Event\Event $event The beforeDelete event that was fired + * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved + * @return void + */ + public function beforeDelete(Event $event, EntityInterface $entity) + { + $config = $this->getConfig(); + $this->_ensureFields($entity); + $left = $entity->get($config['left']); + $right = $entity->get($config['right']); + $diff = $right - $left + 1; + + if ($diff > 2) { + $query = $this->_scope($this->_table->query()) + ->delete() + ->where(function ($exp) use ($config, $left, $right) { + /* @var \Cake\Database\Expression\QueryExpression $exp */ + return $exp + ->gte($config['leftField'], $left + 1) + ->lte($config['leftField'], $right - 1); + }); + $statement = $query->execute(); + $statement->closeCursor(); + } + + $this->_sync($diff, '-', "> {$right}"); + } + + /** + * Sets the correct left and right values for the passed entity so it can be + * updated to a new parent. It also makes the hole in the tree so the node + * move can be done without corrupting the structure. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to re-parent + * @param mixed $parent the id of the parent to set + * @return void + * @throws \RuntimeException if the parent to set to the entity is not valid + */ + protected function _setParent($entity, $parent) + { + $config = $this->getConfig(); + $parentNode = $this->_getNode($parent); + $this->_ensureFields($entity); + $parentLeft = $parentNode->get($config['left']); + $parentRight = $parentNode->get($config['right']); + $right = $entity->get($config['right']); + $left = $entity->get($config['left']); + + if ($parentLeft > $left && $parentLeft < $right) { + throw new RuntimeException(sprintf( + 'Cannot use node "%s" as parent for entity "%s"', + $parent, + $entity->get($this->_getPrimaryKey()) + )); + } + + // Values for moving to the left + $diff = $right - $left + 1; + $targetLeft = $parentRight; + $targetRight = $diff + $parentRight - 1; + $min = $parentRight; + $max = $left - 1; + + if ($left < $targetLeft) { + // Moving to the right + $targetLeft = $parentRight - $diff; + $targetRight = $parentRight - 1; + $min = $right + 1; + $max = $parentRight - 1; + $diff *= -1; + } + + if ($right - $left > 1) { + // Correcting internal subtree + $internalLeft = $left + 1; + $internalRight = $right - 1; + $this->_sync($targetLeft - $left, '+', "BETWEEN {$internalLeft} AND {$internalRight}", true); + } + + $this->_sync($diff, '+', "BETWEEN {$min} AND {$max}"); + + if ($right - $left > 1) { + $this->_unmarkInternalTree(); + } + + // Allocating new position + $entity->set($config['left'], $targetLeft); + $entity->set($config['right'], $targetRight); + } + + /** + * Updates the left and right column for the passed entity so it can be set as + * a new root in the tree. It also modifies the ordering in the rest of the tree + * so the structure remains valid + * + * @param \Cake\Datasource\EntityInterface $entity The entity to set as a new root + * @return void + */ + protected function _setAsRoot($entity) + { + $config = $this->getConfig(); + $edge = $this->_getMax(); + $this->_ensureFields($entity); + $right = $entity->get($config['right']); + $left = $entity->get($config['left']); + $diff = $right - $left; + + if ($right - $left > 1) { + //Correcting internal subtree + $internalLeft = $left + 1; + $internalRight = $right - 1; + $this->_sync($edge - $diff - $left, '+', "BETWEEN {$internalLeft} AND {$internalRight}", true); + } + + $this->_sync($diff + 1, '-', "BETWEEN {$right} AND {$edge}"); + + if ($right - $left > 1) { + $this->_unmarkInternalTree(); + } + + $entity->set($config['left'], $edge - $diff); + $entity->set($config['right'], $edge); + } + + /** + * Helper method used to invert the sign of the left and right columns that are + * less than 0. They were set to negative values before so their absolute value + * wouldn't change while performing other tree transformations. + * + * @return void + */ + protected function _unmarkInternalTree() + { + $config = $this->getConfig(); + $this->_table->updateAll( + function ($exp) use ($config) { + /* @var \Cake\Database\Expression\QueryExpression $exp */ + $leftInverse = clone $exp; + $leftInverse->setConjunction('*')->add('-1'); + $rightInverse = clone $leftInverse; + + return $exp + ->eq($config['leftField'], $leftInverse->add($config['leftField'])) + ->eq($config['rightField'], $rightInverse->add($config['rightField'])); + }, + function ($exp) use ($config) { + /* @var \Cake\Database\Expression\QueryExpression $exp */ + return $exp->lt($config['leftField'], 0); + } + ); + } + + /** + * Custom finder method which can be used to return the list of nodes from the root + * to a specific node in the tree. This custom finder requires that the key 'for' + * is passed in the options containing the id of the node to get its path for. + * + * @param \Cake\ORM\Query $query The constructed query to modify + * @param array $options the list of options for the query + * @return \Cake\ORM\Query + * @throws \InvalidArgumentException If the 'for' key is missing in options + */ + public function findPath(Query $query, array $options) + { + if (empty($options['for'])) { + throw new InvalidArgumentException("The 'for' key is required for find('path')"); + } + + $config = $this->getConfig(); + list($left, $right) = array_map( + function ($field) { + return $this->_table->aliasField($field); + }, + [$config['left'], $config['right']] + ); + + $node = $this->_table->get($options['for'], ['fields' => [$left, $right]]); + + return $this->_scope($query) + ->where([ + "$left <=" => $node->get($config['left']), + "$right >=" => $node->get($config['right']), + ]) + ->order([$left => 'ASC']); + } + + /** + * Get the number of children nodes. + * + * @param \Cake\Datasource\EntityInterface $node The entity to count children for + * @param bool $direct whether to count all nodes in the subtree or just + * direct children + * @return int Number of children nodes. + */ + public function childCount(EntityInterface $node, $direct = false) + { + $config = $this->getConfig(); + $parent = $this->_table->aliasField($config['parent']); + + if ($direct) { + return $this->_scope($this->_table->find()) + ->where([$parent => $node->get($this->_getPrimaryKey())]) + ->count(); + } + + $this->_ensureFields($node); + + return ($node->get($config['right']) - $node->get($config['left']) - 1) / 2; + } + + /** + * Get the children nodes of the current model + * + * Available options are: + * + * - for: The id of the record to read. + * - direct: Boolean, whether to return only the direct (true), or all (false) children, + * defaults to false (all children). + * + * If the direct option is set to true, only the direct children are returned (based upon the parent_id field) + * + * @param \Cake\ORM\Query $query Query. + * @param array $options Array of options as described above + * @return \Cake\ORM\Query + * @throws \InvalidArgumentException When the 'for' key is not passed in $options + */ + public function findChildren(Query $query, array $options) + { + $config = $this->getConfig(); + $options += ['for' => null, 'direct' => false]; + list($parent, $left, $right) = array_map( + function ($field) { + return $this->_table->aliasField($field); + }, + [$config['parent'], $config['left'], $config['right']] + ); + + list($for, $direct) = [$options['for'], $options['direct']]; + + if (empty($for)) { + throw new InvalidArgumentException("The 'for' key is required for find('children')"); + } + + if ($query->clause('order') === null) { + $query->order([$left => 'ASC']); + } + + if ($direct) { + return $this->_scope($query)->where([$parent => $for]); + } + + $node = $this->_getNode($for); + + return $this->_scope($query) + ->where([ + "{$right} <" => $node->get($config['right']), + "{$left} >" => $node->get($config['left']), + ]); + } + + /** + * Gets a representation of the elements in the tree as a flat list where the keys are + * the primary key for the table and the values are the display field for the table. + * Values are prefixed to visually indicate relative depth in the tree. + * + * ### Options + * + * - keyPath: A dot separated path to fetch the field to use for the array key, or a closure to + * return the key out of the provided row. + * - valuePath: A dot separated path to fetch the field to use for the array value, or a closure to + * return the value out of the provided row. + * - spacer: A string to be used as prefix for denoting the depth in the tree for each item + * + * @param \Cake\ORM\Query $query Query. + * @param array $options Array of options as described above. + * @return \Cake\ORM\Query + */ + public function findTreeList(Query $query, array $options) + { + $left = $this->_table->aliasField($this->getConfig('left')); + + $results = $this->_scope($query) + ->find('threaded', [ + 'parentField' => $this->getConfig('parent'), + 'order' => [$left => 'ASC'], + ]); + + return $this->formatTreeList($results, $options); + } + + /** + * Formats query as a flat list where the keys are the primary key for the table + * and the values are the display field for the table. Values are prefixed to visually + * indicate relative depth in the tree. + * + * ### Options + * + * - keyPath: A dot separated path to the field that will be the result array key, or a closure to + * return the key from the provided row. + * - valuePath: A dot separated path to the field that is the array's value, or a closure to + * return the value from the provided row. + * - spacer: A string to be used as prefix for denoting the depth in the tree for each item. + * + * @param \Cake\ORM\Query $query The query object to format. + * @param array $options Array of options as described above. + * @return \Cake\ORM\Query Augmented query. + */ + public function formatTreeList(Query $query, array $options = []) + { + return $query->formatResults(function ($results) use ($options) { + /* @var \Cake\Collection\CollectionTrait $results */ + $options += [ + 'keyPath' => $this->_getPrimaryKey(), + 'valuePath' => $this->_table->getDisplayField(), + 'spacer' => '_', + ]; + + return $results + ->listNested() + ->printer($options['valuePath'], $options['keyPath'], $options['spacer']); + }); + } + + /** + * Removes the current node from the tree, by positioning it as a new root + * and re-parents all children up one level. + * + * Note that the node will not be deleted just moved away from its current position + * without moving its children with it. + * + * @param \Cake\Datasource\EntityInterface $node The node to remove from the tree + * @return \Cake\Datasource\EntityInterface|false the node after being removed from the tree or + * false on error + */ + public function removeFromTree(EntityInterface $node) + { + return $this->_table->getConnection()->transactional(function () use ($node) { + $this->_ensureFields($node); + + return $this->_removeFromTree($node); + }); + } + + /** + * Helper function containing the actual code for removeFromTree + * + * @param \Cake\Datasource\EntityInterface $node The node to remove from the tree + * @return \Cake\Datasource\EntityInterface|false the node after being removed from the tree or + * false on error + */ + protected function _removeFromTree($node) + { + $config = $this->getConfig(); + $left = $node->get($config['left']); + $right = $node->get($config['right']); + $parent = $node->get($config['parent']); + + $node->set($config['parent'], null); + + if ($right - $left == 1) { + return $this->_table->save($node); + } + + $primary = $this->_getPrimaryKey(); + $this->_table->updateAll( + [$config['parent'] => $parent], + [$config['parent'] => $node->get($primary)] + ); + $this->_sync(1, '-', 'BETWEEN ' . ($left + 1) . ' AND ' . ($right - 1)); + $this->_sync(2, '-', "> {$right}"); + $edge = $this->_getMax(); + $node->set($config['left'], $edge + 1); + $node->set($config['right'], $edge + 2); + $fields = [$config['parent'], $config['left'], $config['right']]; + + $this->_table->updateAll($node->extract($fields), [$primary => $node->get($primary)]); + + foreach ($fields as $field) { + $node->setDirty($field, false); + } + + return $node; + } + + /** + * Reorders the node without changing its parent. + * + * If the node is the first child, or is a top level node with no previous node + * this method will return false + * + * @param \Cake\Datasource\EntityInterface $node The node to move + * @param int|bool $number How many places to move the node, or true to move to first position + * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found + * @return \Cake\Datasource\EntityInterface|bool $node The node after being moved or false on failure + */ + public function moveUp(EntityInterface $node, $number = 1) + { + if ($number < 1) { + return false; + } + + return $this->_table->getConnection()->transactional(function () use ($node, $number) { + $this->_ensureFields($node); + + return $this->_moveUp($node, $number); + }); + } + + /** + * Helper function used with the actual code for moveUp + * + * @param \Cake\Datasource\EntityInterface $node The node to move + * @param int|bool $number How many places to move the node, or true to move to first position + * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found + * @return \Cake\Datasource\EntityInterface|bool $node The node after being moved or false on failure + */ + protected function _moveUp($node, $number) + { + $config = $this->getConfig(); + list($parent, $left, $right) = [$config['parent'], $config['left'], $config['right']]; + list($nodeParent, $nodeLeft, $nodeRight) = array_values($node->extract([$parent, $left, $right])); + + $targetNode = null; + if ($number !== true) { + $targetNode = $this->_scope($this->_table->find()) + ->select([$left, $right]) + ->where(["$parent IS" => $nodeParent]) + ->where(function ($exp) use ($config, $nodeLeft) { + /* @var \Cake\Database\Expression\QueryExpression $exp */ + return $exp->lt($config['rightField'], $nodeLeft); + }) + ->orderDesc($config['leftField']) + ->offset($number - 1) + ->limit(1) + ->first(); + } + if (!$targetNode) { + $targetNode = $this->_scope($this->_table->find()) + ->select([$left, $right]) + ->where(["$parent IS" => $nodeParent]) + ->where(function ($exp) use ($config, $nodeLeft) { + /* @var \Cake\Database\Expression\QueryExpression $exp */ + return $exp->lt($config['rightField'], $nodeLeft); + }) + ->orderAsc($config['leftField']) + ->limit(1) + ->first(); + + if (!$targetNode) { + return $node; + } + } + + list($targetLeft) = array_values($targetNode->extract([$left, $right])); + $edge = $this->_getMax(); + $leftBoundary = $targetLeft; + $rightBoundary = $nodeLeft - 1; + + $nodeToEdge = $edge - $nodeLeft + 1; + $shift = $nodeRight - $nodeLeft + 1; + $nodeToHole = $edge - $leftBoundary + 1; + $this->_sync($nodeToEdge, '+', "BETWEEN {$nodeLeft} AND {$nodeRight}"); + $this->_sync($shift, '+', "BETWEEN {$leftBoundary} AND {$rightBoundary}"); + $this->_sync($nodeToHole, '-', "> {$edge}"); + + $node->set($left, $targetLeft); + $node->set($right, $targetLeft + ($nodeRight - $nodeLeft)); + + $node->setDirty($left, false); + $node->setDirty($right, false); + + return $node; + } + + /** + * Reorders the node without changing the parent. + * + * If the node is the last child, or is a top level node with no subsequent node + * this method will return false + * + * @param \Cake\Datasource\EntityInterface $node The node to move + * @param int|bool $number How many places to move the node or true to move to last position + * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found + * @return \Cake\Datasource\EntityInterface|bool the entity after being moved or false on failure + */ + public function moveDown(EntityInterface $node, $number = 1) + { + if ($number < 1) { + return false; + } + + return $this->_table->getConnection()->transactional(function () use ($node, $number) { + $this->_ensureFields($node); + + return $this->_moveDown($node, $number); + }); + } + + /** + * Helper function used with the actual code for moveDown + * + * @param \Cake\Datasource\EntityInterface $node The node to move + * @param int|bool $number How many places to move the node, or true to move to last position + * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found + * @return \Cake\Datasource\EntityInterface|bool $node The node after being moved or false on failure + */ + protected function _moveDown($node, $number) + { + $config = $this->getConfig(); + list($parent, $left, $right) = [$config['parent'], $config['left'], $config['right']]; + list($nodeParent, $nodeLeft, $nodeRight) = array_values($node->extract([$parent, $left, $right])); + + $targetNode = null; + if ($number !== true) { + $targetNode = $this->_scope($this->_table->find()) + ->select([$left, $right]) + ->where(["$parent IS" => $nodeParent]) + ->where(function ($exp) use ($config, $nodeRight) { + /* @var \Cake\Database\Expression\QueryExpression $exp */ + return $exp->gt($config['leftField'], $nodeRight); + }) + ->orderAsc($config['leftField']) + ->offset($number - 1) + ->limit(1) + ->first(); + } + if (!$targetNode) { + $targetNode = $this->_scope($this->_table->find()) + ->select([$left, $right]) + ->where(["$parent IS" => $nodeParent]) + ->where(function ($exp) use ($config, $nodeRight) { + /* @var \Cake\Database\Expression\QueryExpression $exp */ + return $exp->gt($config['leftField'], $nodeRight); + }) + ->orderDesc($config['leftField']) + ->limit(1) + ->first(); + + if (!$targetNode) { + return $node; + } + } + + list(, $targetRight) = array_values($targetNode->extract([$left, $right])); + $edge = $this->_getMax(); + $leftBoundary = $nodeRight + 1; + $rightBoundary = $targetRight; + + $nodeToEdge = $edge - $nodeLeft + 1; + $shift = $nodeRight - $nodeLeft + 1; + $nodeToHole = $edge - $rightBoundary + $shift; + $this->_sync($nodeToEdge, '+', "BETWEEN {$nodeLeft} AND {$nodeRight}"); + $this->_sync($shift, '-', "BETWEEN {$leftBoundary} AND {$rightBoundary}"); + $this->_sync($nodeToHole, '-', "> {$edge}"); + + $node->set($left, $targetRight - ($nodeRight - $nodeLeft)); + $node->set($right, $targetRight); + + $node->setDirty($left, false); + $node->setDirty($right, false); + + return $node; + } + + /** + * Returns a single node from the tree from its primary key + * + * @param mixed $id Record id. + * @return \Cake\Datasource\EntityInterface + * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found + */ + protected function _getNode($id) + { + $config = $this->getConfig(); + list($parent, $left, $right) = [$config['parent'], $config['left'], $config['right']]; + $primaryKey = $this->_getPrimaryKey(); + $fields = [$parent, $left, $right]; + if ($config['level']) { + $fields[] = $config['level']; + } + + $node = $this->_scope($this->_table->find()) + ->select($fields) + ->where([$this->_table->aliasField($primaryKey) => $id]) + ->first(); + + if (!$node) { + throw new RecordNotFoundException("Node \"{$id}\" was not found in the tree."); + } + + return $node; + } + + /** + * Recovers the lft and right column values out of the hierarchy defined by the + * parent column. + * + * @return void + */ + public function recover() + { + $this->_table->getConnection()->transactional(function () { + $this->_recoverTree(); + }); + } + + /** + * Recursive method used to recover a single level of the tree + * + * @param int $counter The Last left column value that was assigned + * @param mixed $parentId the parent id of the level to be recovered + * @param int $level Node level + * @return int The next value to use for the left column + */ + protected function _recoverTree($counter = 0, $parentId = null, $level = -1) + { + $config = $this->getConfig(); + list($parent, $left, $right) = [$config['parent'], $config['left'], $config['right']]; + $primaryKey = $this->_getPrimaryKey(); + $aliasedPrimaryKey = $this->_table->aliasField($primaryKey); + $order = $config['recoverOrder'] ?: $aliasedPrimaryKey; + + $query = $this->_scope($this->_table->query()) + ->select([$aliasedPrimaryKey]) + ->where([$this->_table->aliasField($parent) . ' IS' => $parentId]) + ->order($order) + ->enableHydration(false); + + $leftCounter = $counter; + $nextLevel = $level + 1; + foreach ($query as $row) { + $counter++; + $counter = $this->_recoverTree($counter, $row[$primaryKey], $nextLevel); + } + + if ($parentId === null) { + return $counter; + } + + $fields = [$left => $leftCounter, $right => $counter + 1]; + if ($config['level']) { + $fields[$config['level']] = $level; + } + + $this->_table->updateAll( + $fields, + [$primaryKey => $parentId] + ); + + return $counter + 1; + } + + /** + * Returns the maximum index value in the table. + * + * @return int + */ + protected function _getMax() + { + $field = $this->_config['right']; + $rightField = $this->_config['rightField']; + $edge = $this->_scope($this->_table->find()) + ->select([$field]) + ->orderDesc($rightField) + ->first(); + + if (empty($edge->{$field})) { + return 0; + } + + return $edge->{$field}; + } + + /** + * Auxiliary function used to automatically alter the value of both the left and + * right columns by a certain amount that match the passed conditions + * + * @param int $shift the value to use for operating the left and right columns + * @param string $dir The operator to use for shifting the value (+/-) + * @param string $conditions a SQL snipped to be used for comparing left or right + * against it. + * @param bool $mark whether to mark the updated values so that they can not be + * modified by future calls to this function. + * @return void + */ + protected function _sync($shift, $dir, $conditions, $mark = false) + { + $config = $this->_config; + + foreach ([$config['leftField'], $config['rightField']] as $field) { + $query = $this->_scope($this->_table->query()); + $exp = $query->newExpr(); + + $movement = clone $exp; + $movement->add($field)->add((string)$shift)->setConjunction($dir); + + $inverse = clone $exp; + $movement = $mark ? + $inverse->add($movement)->setConjunction('*')->add('-1') : + $movement; + + $where = clone $exp; + $where->add($field)->add($conditions)->setConjunction(''); + + $query->update() + ->set($exp->eq($field, $movement)) + ->where($where); + + $query->execute()->closeCursor(); + } + } + + /** + * Alters the passed query so that it only returns scoped records as defined + * in the tree configuration. + * + * @param \Cake\ORM\Query $query the Query to modify + * @return \Cake\ORM\Query + */ + protected function _scope($query) + { + $scope = $this->getConfig('scope'); + + if (is_array($scope)) { + return $query->where($scope); + } + if (is_callable($scope)) { + return $scope($query); + } + + return $query; + } + + /** + * Ensures that the provided entity contains non-empty values for the left and + * right fields + * + * @param \Cake\Datasource\EntityInterface $entity The entity to ensure fields for + * @return void + */ + protected function _ensureFields($entity) + { + $config = $this->getConfig(); + $fields = [$config['left'], $config['right']]; + $values = array_filter($entity->extract($fields)); + if (count($values) === count($fields)) { + return; + } + + $fresh = $this->_table->get($entity->get($this->_getPrimaryKey()), $fields); + $entity->set($fresh->extract($fields), ['guard' => false]); + + foreach ($fields as $field) { + $entity->setDirty($field, false); + } + } + + /** + * Returns a single string value representing the primary key of the attached table + * + * @return string + */ + protected function _getPrimaryKey() + { + if (!$this->_primaryKey) { + $primaryKey = (array)$this->_table->getPrimaryKey(); + $this->_primaryKey = $primaryKey[0]; + } + + return $this->_primaryKey; + } + + /** + * Returns the depth level of a node in the tree. + * + * @param int|string|\Cake\Datasource\EntityInterface $entity The entity or primary key get the level of. + * @return int|bool Integer of the level or false if the node does not exist. + */ + public function getLevel($entity) + { + $primaryKey = $this->_getPrimaryKey(); + $id = $entity; + if ($entity instanceof EntityInterface) { + $id = $entity->get($primaryKey); + } + $config = $this->getConfig(); + $entity = $this->_table->find('all') + ->select([$config['left'], $config['right']]) + ->where([$primaryKey => $id]) + ->first(); + + if ($entity === null) { + return false; + } + + $query = $this->_table->find('all')->where([ + $config['left'] . ' <' => $entity[$config['left']], + $config['right'] . ' >' => $entity[$config['right']], + ]); + + return $this->_scope($query)->count(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/BehaviorRegistry.php b/app/vendor/cakephp/cakephp/src/ORM/BehaviorRegistry.php new file mode 100644 index 000000000..a72389e6e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/BehaviorRegistry.php @@ -0,0 +1,283 @@ +setTable($table); + } + } + + /** + * Attaches a table instance to this registry. + * + * @param \Cake\ORM\Table $table The table this registry is attached to. + * @return void + */ + public function setTable(Table $table) + { + $this->_table = $table; + $eventManager = $table->getEventManager(); + if ($eventManager !== null) { + $this->setEventManager($eventManager); + } + } + + /** + * Resolve a behavior classname. + * + * @param string $class Partial classname to resolve. + * @return string|null Either the correct classname or null. + * @since 3.5.7 + */ + public static function className($class) + { + $result = App::className($class, 'Model/Behavior', 'Behavior'); + if (!$result) { + $result = App::className($class, 'ORM/Behavior', 'Behavior'); + } + + return $result ?: null; + } + + /** + * Resolve a behavior classname. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * + * @param string $class Partial classname to resolve. + * @return string|false Either the correct classname or false. + */ + protected function _resolveClassName($class) + { + return static::className($class) ?: false; + } + + /** + * Throws an exception when a behavior is missing. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * and Cake\Core\ObjectRegistry::unload() + * + * @param string $class The classname that is missing. + * @param string $plugin The plugin the behavior is missing in. + * @return void + * @throws \Cake\ORM\Exception\MissingBehaviorException + */ + protected function _throwMissingClassError($class, $plugin) + { + throw new MissingBehaviorException([ + 'class' => $class . 'Behavior', + 'plugin' => $plugin + ]); + } + + /** + * Create the behavior instance. + * + * Part of the template method for Cake\Core\ObjectRegistry::load() + * Enabled behaviors will be registered with the event manager. + * + * @param string $class The classname that is missing. + * @param string $alias The alias of the object. + * @param array $config An array of config to use for the behavior. + * @return \Cake\ORM\Behavior The constructed behavior class. + */ + protected function _create($class, $alias, $config) + { + $instance = new $class($this->_table, $config); + $enable = isset($config['enabled']) ? $config['enabled'] : true; + if ($enable) { + $this->getEventManager()->on($instance); + } + $methods = $this->_getMethods($instance, $class, $alias); + $this->_methodMap += $methods['methods']; + $this->_finderMap += $methods['finders']; + + return $instance; + } + + /** + * Get the behavior methods and ensure there are no duplicates. + * + * Use the implementedEvents() method to exclude callback methods. + * Methods starting with `_` will be ignored, as will methods + * declared on Cake\ORM\Behavior + * + * @param \Cake\ORM\Behavior $instance The behavior to get methods from. + * @param string $class The classname that is missing. + * @param string $alias The alias of the object. + * @return array A list of implemented finders and methods. + * @throws \LogicException when duplicate methods are connected. + */ + protected function _getMethods(Behavior $instance, $class, $alias) + { + $finders = array_change_key_case($instance->implementedFinders()); + $methods = array_change_key_case($instance->implementedMethods()); + + foreach ($finders as $finder => $methodName) { + if (isset($this->_finderMap[$finder]) && $this->has($this->_finderMap[$finder][0])) { + $duplicate = $this->_finderMap[$finder]; + $error = sprintf( + '%s contains duplicate finder "%s" which is already provided by "%s"', + $class, + $finder, + $duplicate[0] + ); + throw new LogicException($error); + } + $finders[$finder] = [$alias, $methodName]; + } + + foreach ($methods as $method => $methodName) { + if (isset($this->_methodMap[$method]) && $this->has($this->_methodMap[$method][0])) { + $duplicate = $this->_methodMap[$method]; + $error = sprintf( + '%s contains duplicate method "%s" which is already provided by "%s"', + $class, + $method, + $duplicate[0] + ); + throw new LogicException($error); + } + $methods[$method] = [$alias, $methodName]; + } + + return compact('methods', 'finders'); + } + + /** + * Check if any loaded behavior implements a method. + * + * Will return true if any behavior provides a public non-finder method + * with the chosen name. + * + * @param string $method The method to check for. + * @return bool + */ + public function hasMethod($method) + { + $method = strtolower($method); + + return isset($this->_methodMap[$method]); + } + + /** + * Check if any loaded behavior implements the named finder. + * + * Will return true if any behavior provides a public method with + * the chosen name. + * + * @param string $method The method to check for. + * @return bool + */ + public function hasFinder($method) + { + $method = strtolower($method); + + return isset($this->_finderMap[$method]); + } + + /** + * Invoke a method on a behavior. + * + * @param string $method The method to invoke. + * @param array $args The arguments you want to invoke the method with. + * @return mixed The return value depends on the underlying behavior method. + * @throws \BadMethodCallException When the method is unknown. + */ + public function call($method, array $args = []) + { + $method = strtolower($method); + if ($this->hasMethod($method) && $this->has($this->_methodMap[$method][0])) { + list($behavior, $callMethod) = $this->_methodMap[$method]; + + return call_user_func_array([$this->_loaded[$behavior], $callMethod], $args); + } + + throw new BadMethodCallException( + sprintf('Cannot call "%s" it does not belong to any attached behavior.', $method) + ); + } + + /** + * Invoke a finder on a behavior. + * + * @param string $type The finder type to invoke. + * @param array $args The arguments you want to invoke the method with. + * @return mixed The return value depends on the underlying behavior method. + * @throws \BadMethodCallException When the method is unknown. + */ + public function callFinder($type, array $args = []) + { + $type = strtolower($type); + + if ($this->hasFinder($type) && $this->has($this->_finderMap[$type][0])) { + list($behavior, $callMethod) = $this->_finderMap[$type]; + + return call_user_func_array([$this->_loaded[$behavior], $callMethod], $args); + } + + throw new BadMethodCallException( + sprintf('Cannot call finder "%s" it does not belong to any attached behavior.', $type) + ); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/EagerLoadable.php b/app/vendor/cakephp/cakephp/src/ORM/EagerLoadable.php new file mode 100644 index 000000000..9d93f5b1b --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/EagerLoadable.php @@ -0,0 +1,344 @@ +author->company->country + * ``` + * + * The property path of `country` will be `author.company` + * + * @var string + */ + protected $_propertyPath; + + /** + * Whether or not this level can be fetched using a join. + * + * @var bool + */ + protected $_canBeJoined = false; + + /** + * Whether or not this level was meant for a "matching" fetch + * operation + * + * @var bool + */ + protected $_forMatching; + + /** + * The property name where the association result should be nested + * in the result. + * + * For example, in the following nested property: + * + * ``` + * $article->author->company->country + * ``` + * + * The target property of `country` will be just `country` + * + * @var string + */ + protected $_targetProperty; + + /** + * Constructor. The $config parameter accepts the following array + * keys: + * + * - associations + * - instance + * - config + * - canBeJoined + * - aliasPath + * - propertyPath + * - forMatching + * - targetProperty + * + * The keys maps to the settable properties in this class. + * + * @param string $name The Association name. + * @param array $config The list of properties to set. + */ + public function __construct($name, array $config = []) + { + $this->_name = $name; + $allowed = [ + 'associations', 'instance', 'config', 'canBeJoined', + 'aliasPath', 'propertyPath', 'forMatching', 'targetProperty' + ]; + foreach ($allowed as $property) { + if (isset($config[$property])) { + $this->{'_' . $property} = $config[$property]; + } + } + } + + /** + * Adds a new association to be loaded from this level. + * + * @param string $name The association name. + * @param \Cake\ORM\EagerLoadable $association The association to load. + * @return void + */ + public function addAssociation($name, EagerLoadable $association) + { + $this->_associations[$name] = $association; + } + + /** + * Returns the Association class instance to use for loading the records. + * + * @return array + */ + public function associations() + { + return $this->_associations; + } + + /** + * Gets the Association class instance to use for loading the records. + * + * @return \Cake\ORM\Association|null + */ + public function instance() + { + return $this->_instance; + } + + /** + * Gets a dot separated string representing the path of associations + * that should be followed to fetch this level. + * + * @return string|null + */ + public function aliasPath() + { + return $this->_aliasPath; + } + + /** + * Gets a dot separated string representing the path of entity properties + * in which results for this level should be placed. + * + * For example, in the following nested property: + * + * ``` + * $article->author->company->country + * ``` + * + * The property path of `country` will be `author.company` + * + * @return string|null + */ + public function propertyPath() + { + return $this->_propertyPath; + } + + /** + * Sets whether or not this level can be fetched using a join. + * + * @param bool $possible The value to set. + * @return $this + */ + public function setCanBeJoined($possible) + { + $this->_canBeJoined = (bool)$possible; + + return $this; + } + + /** + * Gets whether or not this level can be fetched using a join. + * + * If called with arguments it sets the value. + * As of 3.4.0 the setter part is deprecated, use setCanBeJoined() instead. + * + * @param bool|null $possible The value to set. + * @return bool + */ + public function canBeJoined($possible = null) + { + if ($possible !== null) { + deprecationWarning( + 'Using EagerLoadable::canBeJoined() as a setter is deprecated. ' . + 'Use setCanBeJoined() instead.' + ); + $this->setCanBeJoined($possible); + } + + return $this->_canBeJoined; + } + + /** + * Sets the list of options to pass to the association object for loading + * the records. + * + * @param array $config The value to set. + * @return $this + */ + public function setConfig(array $config) + { + $this->_config = $config; + + return $this; + } + + /** + * Gets the list of options to pass to the association object for loading + * the records. + * + * @return array + */ + public function getConfig() + { + return $this->_config; + } + + /** + * Sets the list of options to pass to the association object for loading + * the records. + * + * If called with no arguments it returns the current + * value. + * + * @deprecated 3.4.0 Use setConfig()/getConfig() instead. + * @param array|null $config The value to set. + * @return array + */ + public function config(array $config = null) + { + deprecationWarning( + 'EagerLoadable::config() is deprecated. ' . + 'Use setConfig()/getConfig() instead.' + ); + if ($config !== null) { + $this->setConfig($config); + } + + return $this->getConfig(); + } + + /** + * Gets whether or not this level was meant for a + * "matching" fetch operation. + * + * @return bool|null + */ + public function forMatching() + { + return $this->_forMatching; + } + + /** + * The property name where the result of this association + * should be nested at the end. + * + * For example, in the following nested property: + * + * ``` + * $article->author->company->country + * ``` + * + * The target property of `country` will be just `country` + * + * @return string|null + */ + public function targetProperty() + { + return $this->_targetProperty; + } + + /** + * Returns a representation of this object that can be passed to + * Cake\ORM\EagerLoader::contain() + * + * @return array + */ + public function asContainArray() + { + $associations = []; + foreach ($this->_associations as $assoc) { + $associations += $assoc->asContainArray(); + } + $config = $this->_config; + if ($this->_forMatching !== null) { + $config = ['matching' => $this->_forMatching] + $config; + } + + return [ + $this->_name => [ + 'associations' => $associations, + 'config' => $config + ] + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/EagerLoader.php b/app/vendor/cakephp/cakephp/src/ORM/EagerLoader.php new file mode 100644 index 000000000..749f43665 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/EagerLoader.php @@ -0,0 +1,891 @@ + 1, + 'foreignKey' => 1, + 'conditions' => 1, + 'fields' => 1, + 'sort' => 1, + 'matching' => 1, + 'queryBuilder' => 1, + 'finder' => 1, + 'joinType' => 1, + 'strategy' => 1, + 'negateMatch' => 1 + ]; + + /** + * A list of associations that should be loaded with a separate query + * + * @var \Cake\ORM\EagerLoadable[] + */ + protected $_loadExternal = []; + + /** + * Contains a list of the association names that are to be eagerly loaded + * + * @var array + */ + protected $_aliasList = []; + + /** + * Another EagerLoader instance that will be used for 'matching' associations. + * + * @var \Cake\ORM\EagerLoader + */ + protected $_matching; + + /** + * A map of table aliases pointing to the association objects they represent + * for the query. + * + * @var array + */ + protected $_joinsMap = []; + + /** + * Controls whether or not fields from associated tables + * will be eagerly loaded. When set to false, no fields will + * be loaded from associations. + * + * @var bool + */ + protected $_autoFields = true; + + /** + * Sets the list of associations that should be eagerly loaded along for a + * specific table using when a query is provided. The list of associated tables + * passed to this method must have been previously set as associations using the + * Table API. + * + * Associations can be arbitrarily nested using dot notation or nested arrays, + * this allows this object to calculate joins or any additional queries that + * must be executed to bring the required associated data. + * + * The getter part is deprecated as of 3.6.0. Use getContain() instead. + * + * Accepted options per passed association: + * + * - foreignKey: Used to set a different field to match both tables, if set to false + * no join conditions will be generated automatically + * - fields: An array with the fields that should be fetched from the association + * - queryBuilder: Equivalent to passing a callable instead of an options array + * - matching: Whether to inform the association class that it should filter the + * main query by the results fetched by that class. + * - joinType: For joinable associations, the SQL join type to use. + * - strategy: The loading strategy to use (join, select, subquery) + * + * @param array|string $associations list of table aliases to be queried. + * When this method is called multiple times it will merge previous list with + * the new one. + * @param callable|null $queryBuilder The query builder callable + * @return array Containments. + * @throws \InvalidArgumentException When using $queryBuilder with an array of $associations + */ + public function contain($associations = [], callable $queryBuilder = null) + { + if (empty($associations)) { + deprecationWarning( + 'Using EagerLoader::contain() as getter is deprecated. ' . + 'Use getContain() instead.' + ); + + return $this->getContain(); + } + + if ($queryBuilder) { + if (!is_string($associations)) { + throw new InvalidArgumentException( + sprintf('Cannot set containments. To use $queryBuilder, $associations must be a string') + ); + } + + $associations = [ + $associations => [ + 'queryBuilder' => $queryBuilder + ] + ]; + } + + $associations = (array)$associations; + $associations = $this->_reformatContain($associations, $this->_containments); + $this->_normalized = null; + $this->_loadExternal = []; + $this->_aliasList = []; + + return $this->_containments = $associations; + } + + /** + * Gets the list of associations that should be eagerly loaded along for a + * specific table using when a query is provided. The list of associated tables + * passed to this method must have been previously set as associations using the + * Table API. + * + * @return array Containments. + */ + public function getContain() + { + return $this->_containments; + } + + /** + * Remove any existing non-matching based containments. + * + * This will reset/clear out any contained associations that were not + * added via matching(). + * + * @return void + */ + public function clearContain() + { + $this->_containments = []; + $this->_normalized = null; + $this->_loadExternal = []; + $this->_aliasList = []; + } + + /** + * Sets whether or not contained associations will load fields automatically. + * + * @param bool $enable The value to set. + * @return $this + */ + public function enableAutoFields($enable = true) + { + $this->_autoFields = (bool)$enable; + + return $this; + } + + /** + * Gets whether or not contained associations will load fields automatically. + * + * @return bool The current value. + */ + public function isAutoFieldsEnabled() + { + return $this->_autoFields; + } + + /** + * Sets/Gets whether or not contained associations will load fields automatically. + * + * @deprecated 3.4.0 Use enableAutoFields()/isAutoFieldsEnabled() instead. + * @param bool|null $enable The value to set. + * @return bool The current value. + */ + public function autoFields($enable = null) + { + deprecationWarning( + 'EagerLoader::autoFields() is deprecated. ' . + 'Use enableAutoFields()/isAutoFieldsEnabled() instead.' + ); + if ($enable !== null) { + $this->enableAutoFields($enable); + } + + return $this->isAutoFieldsEnabled(); + } + + /** + * Adds a new association to the list that will be used to filter the results of + * any given query based on the results of finding records for that association. + * You can pass a dot separated path of associations to this method as its first + * parameter, this will translate in setting all those associations with the + * `matching` option. + * + * ### Options + * - 'joinType': INNER, OUTER, ... + * - 'fields': Fields to contain + * + * @param string $assoc A single association or a dot separated path of associations. + * @param callable|null $builder the callback function to be used for setting extra + * options to the filtering query + * @param array $options Extra options for the association matching. + * @return $this + */ + public function setMatching($assoc, callable $builder = null, $options = []) + { + if ($this->_matching === null) { + $this->_matching = new static(); + } + + if (!isset($options['joinType'])) { + $options['joinType'] = QueryInterface::JOIN_TYPE_INNER; + } + + $assocs = explode('.', $assoc); + $last = array_pop($assocs); + $containments = []; + $pointer =& $containments; + $opts = ['matching' => true] + $options; + unset($opts['negateMatch']); + + foreach ($assocs as $name) { + $pointer[$name] = $opts; + $pointer =& $pointer[$name]; + } + + $pointer[$last] = ['queryBuilder' => $builder, 'matching' => true] + $options; + + $this->_matching->contain($containments); + + return $this; + } + + /** + * Returns the current tree of associations to be matched. + * + * @return array The resulting containments array + */ + public function getMatching() + { + if ($this->_matching === null) { + $this->_matching = new static(); + } + + return $this->_matching->getContain(); + } + + /** + * Adds a new association to the list that will be used to filter the results of + * any given query based on the results of finding records for that association. + * You can pass a dot separated path of associations to this method as its first + * parameter, this will translate in setting all those associations with the + * `matching` option. + * + * If called with no arguments it will return the current tree of associations to + * be matched. + * + * @deprecated 3.4.0 Use setMatching()/getMatching() instead. + * @param string|null $assoc A single association or a dot separated path of associations. + * @param callable|null $builder the callback function to be used for setting extra + * options to the filtering query + * @param array $options Extra options for the association matching, such as 'joinType' + * and 'fields' + * @return array The resulting containments array + */ + public function matching($assoc = null, callable $builder = null, $options = []) + { + deprecationWarning( + 'EagerLoader::matching() is deprecated. ' . + 'Use setMatch()/getMatching() instead.' + ); + if ($assoc !== null) { + $this->setMatching($assoc, $builder, $options); + } + + return $this->getMatching(); + } + + /** + * Returns the fully normalized array of associations that should be eagerly + * loaded for a table. The normalized array will restructure the original array + * by sorting all associations under one key and special options under another. + * + * Each of the levels of the associations tree will converted to a Cake\ORM\EagerLoadable + * object, that contains all the information required for the association objects + * to load the information from the database. + * + * Additionally it will set an 'instance' key per association containing the + * association instance from the corresponding source table + * + * @param \Cake\ORM\Table $repository The table containing the association that + * will be normalized + * @return array + */ + public function normalized(Table $repository) + { + if ($this->_normalized !== null || empty($this->_containments)) { + return (array)$this->_normalized; + } + + $contain = []; + foreach ($this->_containments as $alias => $options) { + if (!empty($options['instance'])) { + $contain = (array)$this->_containments; + break; + } + $contain[$alias] = $this->_normalizeContain( + $repository, + $alias, + $options, + ['root' => null] + ); + } + + return $this->_normalized = $contain; + } + + /** + * Formats the containments array so that associations are always set as keys + * in the array. This function merges the original associations array with + * the new associations provided + * + * @param array $associations user provided containments array + * @param array $original The original containments array to merge + * with the new one + * @return array + */ + protected function _reformatContain($associations, $original) + { + $result = $original; + + foreach ((array)$associations as $table => $options) { + $pointer =& $result; + if (is_int($table)) { + $table = $options; + $options = []; + } + + if ($options instanceof EagerLoadable) { + $options = $options->asContainArray(); + $table = key($options); + $options = current($options); + } + + if (isset($this->_containOptions[$table])) { + $pointer[$table] = $options; + continue; + } + + if (strpos($table, '.')) { + $path = explode('.', $table); + $table = array_pop($path); + foreach ($path as $t) { + $pointer += [$t => []]; + $pointer =& $pointer[$t]; + } + } + + if (is_array($options)) { + $options = isset($options['config']) ? + $options['config'] + $options['associations'] : + $options; + $options = $this->_reformatContain( + $options, + isset($pointer[$table]) ? $pointer[$table] : [] + ); + } + + if ($options instanceof Closure) { + $options = ['queryBuilder' => $options]; + } + + $pointer += [$table => []]; + + if (isset($options['queryBuilder'], $pointer[$table]['queryBuilder'])) { + $first = $pointer[$table]['queryBuilder']; + $second = $options['queryBuilder']; + $options['queryBuilder'] = function ($query) use ($first, $second) { + return $second($first($query)); + }; + } + + if (!is_array($options)) { + $options = [$options => []]; + } + + $pointer[$table] = $options + $pointer[$table]; + } + + return $result; + } + + /** + * Modifies the passed query to apply joins or any other transformation required + * in order to eager load the associations described in the `contain` array. + * This method will not modify the query for loading external associations, i.e. + * those that cannot be loaded without executing a separate query. + * + * @param \Cake\ORM\Query $query The query to be modified + * @param \Cake\ORM\Table $repository The repository containing the associations + * @param bool $includeFields whether to append all fields from the associations + * to the passed query. This can be overridden according to the settings defined + * per association in the containments array + * @return void + */ + public function attachAssociations(Query $query, Table $repository, $includeFields) + { + if (empty($this->_containments) && $this->_matching === null) { + return; + } + + $attachable = $this->attachableAssociations($repository); + $processed = []; + do { + foreach ($attachable as $alias => $loadable) { + $config = $loadable->getConfig() + [ + 'aliasPath' => $loadable->aliasPath(), + 'propertyPath' => $loadable->propertyPath(), + 'includeFields' => $includeFields, + ]; + $loadable->instance()->attachTo($query, $config); + $processed[$alias] = true; + } + + $newAttachable = $this->attachableAssociations($repository); + $attachable = array_diff_key($newAttachable, $processed); + } while (!empty($attachable)); + } + + /** + * Returns an array with the associations that can be fetched using a single query, + * the array keys are the association aliases and the values will contain an array + * with Cake\ORM\EagerLoadable objects. + * + * @param \Cake\ORM\Table $repository The table containing the associations to be + * attached + * @return array + */ + public function attachableAssociations(Table $repository) + { + $contain = $this->normalized($repository); + $matching = $this->_matching ? $this->_matching->normalized($repository) : []; + $this->_fixStrategies(); + $this->_loadExternal = []; + + return $this->_resolveJoins($contain, $matching); + } + + /** + * Returns an array with the associations that need to be fetched using a + * separate query, each array value will contain a Cake\ORM\EagerLoadable object. + * + * @param \Cake\ORM\Table $repository The table containing the associations + * to be loaded + * @return \Cake\ORM\EagerLoadable[] + */ + public function externalAssociations(Table $repository) + { + if ($this->_loadExternal) { + return $this->_loadExternal; + } + + $this->attachableAssociations($repository); + + return $this->_loadExternal; + } + + /** + * Auxiliary function responsible for fully normalizing deep associations defined + * using `contain()` + * + * @param \Cake\ORM\Table $parent owning side of the association + * @param string $alias name of the association to be loaded + * @param array $options list of extra options to use for this association + * @param array $paths An array with two values, the first one is a list of dot + * separated strings representing associations that lead to this `$alias` in the + * chain of associations to be loaded. The second value is the path to follow in + * entities' properties to fetch a record of the corresponding association. + * @return \Cake\ORM\EagerLoadable Object with normalized associations + * @throws \InvalidArgumentException When containments refer to associations that do not exist. + */ + protected function _normalizeContain(Table $parent, $alias, $options, $paths) + { + $defaults = $this->_containOptions; + $instance = $parent->getAssociation($alias); + if (!$instance) { + throw new InvalidArgumentException( + sprintf('%s is not associated with %s', $parent->getAlias(), $alias) + ); + } + + $paths += ['aliasPath' => '', 'propertyPath' => '', 'root' => $alias]; + $paths['aliasPath'] .= '.' . $alias; + $paths['propertyPath'] .= '.' . $instance->getProperty(); + + $table = $instance->getTarget(); + + $extra = array_diff_key($options, $defaults); + $config = [ + 'associations' => [], + 'instance' => $instance, + 'config' => array_diff_key($options, $extra), + 'aliasPath' => trim($paths['aliasPath'], '.'), + 'propertyPath' => trim($paths['propertyPath'], '.'), + 'targetProperty' => $instance->getProperty() + ]; + $config['canBeJoined'] = $instance->canBeJoined($config['config']); + $eagerLoadable = new EagerLoadable($alias, $config); + + if ($config['canBeJoined']) { + $this->_aliasList[$paths['root']][$alias][] = $eagerLoadable; + } else { + $paths['root'] = $config['aliasPath']; + } + + foreach ($extra as $t => $assoc) { + $eagerLoadable->addAssociation( + $t, + $this->_normalizeContain($table, $t, $assoc, $paths) + ); + } + + return $eagerLoadable; + } + + /** + * Iterates over the joinable aliases list and corrects the fetching strategies + * in order to avoid aliases collision in the generated queries. + * + * This function operates on the array references that were generated by the + * _normalizeContain() function. + * + * @return void + */ + protected function _fixStrategies() + { + foreach ($this->_aliasList as $aliases) { + foreach ($aliases as $configs) { + if (count($configs) < 2) { + continue; + } + /* @var \Cake\ORM\EagerLoadable $loadable */ + foreach ($configs as $loadable) { + if (strpos($loadable->aliasPath(), '.')) { + $this->_correctStrategy($loadable); + } + } + } + } + } + + /** + * Changes the association fetching strategy if required because of duplicate + * under the same direct associations chain + * + * @param \Cake\ORM\EagerLoadable $loadable The association config + * @return void + */ + protected function _correctStrategy($loadable) + { + $config = $loadable->getConfig(); + $currentStrategy = isset($config['strategy']) ? + $config['strategy'] : + 'join'; + + if (!$loadable->canBeJoined() || $currentStrategy !== 'join') { + return; + } + + $config['strategy'] = Association::STRATEGY_SELECT; + $loadable->setConfig($config); + $loadable->setCanBeJoined(false); + } + + /** + * Helper function used to compile a list of all associations that can be + * joined in the query. + * + * @param array $associations list of associations from which to obtain joins. + * @param array $matching list of associations that should be forcibly joined. + * @return array + */ + protected function _resolveJoins($associations, $matching = []) + { + $result = []; + foreach ($matching as $table => $loadable) { + $result[$table] = $loadable; + $result += $this->_resolveJoins($loadable->associations(), []); + } + foreach ($associations as $table => $loadable) { + $inMatching = isset($matching[$table]); + if (!$inMatching && $loadable->canBeJoined()) { + $result[$table] = $loadable; + $result += $this->_resolveJoins($loadable->associations(), []); + continue; + } + + if ($inMatching) { + $this->_correctStrategy($loadable); + } + + $loadable->setCanBeJoined(false); + $this->_loadExternal[] = $loadable; + } + + return $result; + } + + /** + * Decorates the passed statement object in order to inject data from associations + * that cannot be joined directly. + * + * @param \Cake\ORM\Query $query The query for which to eager load external + * associations + * @param \Cake\Database\StatementInterface $statement The statement created after executing the $query + * @return \Cake\Database\StatementInterface statement modified statement with extra loaders + */ + public function loadExternal($query, $statement) + { + $external = $this->externalAssociations($query->getRepository()); + if (empty($external)) { + return $statement; + } + + $driver = $query->getConnection()->getDriver(); + list($collected, $statement) = $this->_collectKeys($external, $query, $statement); + + foreach ($external as $meta) { + $contain = $meta->associations(); + $instance = $meta->instance(); + $config = $meta->getConfig(); + $alias = $instance->getSource()->getAlias(); + $path = $meta->aliasPath(); + + $requiresKeys = $instance->requiresKeys($config); + if ($requiresKeys && empty($collected[$path][$alias])) { + continue; + } + + $keys = isset($collected[$path][$alias]) ? $collected[$path][$alias] : null; + $f = $instance->eagerLoader( + $config + [ + 'query' => $query, + 'contain' => $contain, + 'keys' => $keys, + 'nestKey' => $meta->aliasPath() + ] + ); + $statement = new CallbackStatement($statement, $driver, $f); + } + + return $statement; + } + + /** + * Returns an array having as keys a dotted path of associations that participate + * in this eager loader. The values of the array will contain the following keys + * + * - alias: The association alias + * - instance: The association instance + * - canBeJoined: Whether or not the association will be loaded using a JOIN + * - entityClass: The entity that should be used for hydrating the results + * - nestKey: A dotted path that can be used to correctly insert the data into the results. + * - matching: Whether or not it is an association loaded through `matching()`. + * + * @param \Cake\ORM\Table $table The table containing the association that + * will be normalized + * @return array + */ + public function associationsMap($table) + { + $map = []; + + if (!$this->getMatching() && !$this->getContain() && empty($this->_joinsMap)) { + return $map; + } + + $map = $this->_buildAssociationsMap($map, $this->_matching->normalized($table), true); + $map = $this->_buildAssociationsMap($map, $this->normalized($table)); + $map = $this->_buildAssociationsMap($map, $this->_joinsMap); + + return $map; + } + + /** + * An internal method to build a map which is used for the return value of the + * associationsMap() method. + * + * @param array $map An initial array for the map. + * @param array $level An array of EagerLoadable instances. + * @param bool $matching Whether or not it is an association loaded through `matching()`. + * @return array + */ + protected function _buildAssociationsMap($map, $level, $matching = false) + { + /* @var \Cake\ORM\EagerLoadable $meta */ + foreach ($level as $assoc => $meta) { + $canBeJoined = $meta->canBeJoined(); + $instance = $meta->instance(); + $associations = $meta->associations(); + $forMatching = $meta->forMatching(); + $map[] = [ + 'alias' => $assoc, + 'instance' => $instance, + 'canBeJoined' => $canBeJoined, + 'entityClass' => $instance->getTarget()->getEntityClass(), + 'nestKey' => $canBeJoined ? $assoc : $meta->aliasPath(), + 'matching' => $forMatching !== null ? $forMatching : $matching, + 'targetProperty' => $meta->targetProperty() + ]; + if ($canBeJoined && $associations) { + $map = $this->_buildAssociationsMap($map, $associations, $matching); + } + } + + return $map; + } + + /** + * Registers a table alias, typically loaded as a join in a query, as belonging to + * an association. This helps hydrators know what to do with the columns coming + * from such joined table. + * + * @param string $alias The table alias as it appears in the query. + * @param \Cake\ORM\Association $assoc The association object the alias represents; + * will be normalized + * @param bool $asMatching Whether or not this join results should be treated as a + * 'matching' association. + * @param string $targetProperty The property name where the results of the join should be nested at. + * If not passed, the default property for the association will be used. + * @return void + */ + public function addToJoinsMap($alias, Association $assoc, $asMatching = false, $targetProperty = null) + { + $this->_joinsMap[$alias] = new EagerLoadable($alias, [ + 'aliasPath' => $alias, + 'instance' => $assoc, + 'canBeJoined' => true, + 'forMatching' => $asMatching, + 'targetProperty' => $targetProperty ?: $assoc->getProperty() + ]); + } + + /** + * Helper function used to return the keys from the query records that will be used + * to eagerly load associations. + * + * @param array $external the list of external associations to be loaded + * @param \Cake\ORM\Query $query The query from which the results where generated + * @param \Cake\Database\Statement\BufferedStatement $statement The statement to work on + * @return array + */ + protected function _collectKeys($external, $query, $statement) + { + $collectKeys = []; + /* @var \Cake\ORM\EagerLoadable $meta */ + foreach ($external as $meta) { + $instance = $meta->instance(); + if (!$instance->requiresKeys($meta->getConfig())) { + continue; + } + + $source = $instance->getSource(); + $keys = $instance->type() === Association::MANY_TO_ONE ? + (array)$instance->getForeignKey() : + (array)$instance->getBindingKey(); + + $alias = $source->getAlias(); + $pkFields = []; + foreach ($keys as $key) { + $pkFields[] = key($query->aliasField($key, $alias)); + } + $collectKeys[$meta->aliasPath()] = [$alias, $pkFields, count($pkFields) === 1]; + } + + if (empty($collectKeys)) { + return [[], $statement]; + } + + if (!($statement instanceof BufferedStatement)) { + $statement = new BufferedStatement($statement, $query->getConnection()->getDriver()); + } + + return [$this->_groupKeys($statement, $collectKeys), $statement]; + } + + /** + * Helper function used to iterate a statement and extract the columns + * defined in $collectKeys + * + * @param \Cake\Database\Statement\BufferedStatement $statement The statement to read from. + * @param array $collectKeys The keys to collect + * @return array + */ + protected function _groupKeys($statement, $collectKeys) + { + $keys = []; + while ($result = $statement->fetch('assoc')) { + foreach ($collectKeys as $nestKey => $parts) { + // Missed joins will have null in the results. + if ($parts[2] === true && !isset($result[$parts[1][0]])) { + continue; + } + if ($parts[2] === true) { + $value = $result[$parts[1][0]]; + $keys[$nestKey][$parts[0]][$value] = $value; + continue; + } + + // Handle composite keys. + $collected = []; + foreach ($parts[1] as $key) { + $collected[] = $result[$key]; + } + $keys[$nestKey][$parts[0]][implode(';', $collected)] = $collected; + } + } + + $statement->rewind(); + + return $keys; + } + + /** + * Clone hook implementation + * + * Clone the _matching eager loader as well. + * + * @return void + */ + public function __clone() + { + if ($this->_matching) { + $this->_matching = clone $this->_matching; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Entity.php b/app/vendor/cakephp/cakephp/src/ORM/Entity.php new file mode 100644 index 000000000..a3771491e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Entity.php @@ -0,0 +1,83 @@ + 1, 'name' => 'Andrew']) + * ``` + * + * @param array $properties hash of properties to set in this entity + * @param array $options list of options to use when creating this entity + */ + public function __construct(array $properties = [], array $options = []) + { + $options += [ + 'useSetters' => true, + 'markClean' => false, + 'markNew' => null, + 'guard' => false, + 'source' => null + ]; + + if (!empty($options['source'])) { + $this->setSource($options['source']); + } + + if ($options['markNew'] !== null) { + $this->isNew($options['markNew']); + } + + if (!empty($properties) && $options['markClean'] && !$options['useSetters']) { + $this->_properties = $properties; + + return; + } + + if (!empty($properties)) { + $this->set($properties, [ + 'setter' => $options['useSetters'], + 'guard' => $options['guard'] + ]); + } + + if ($options['markClean']) { + $this->clean(); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Exception/MissingBehaviorException.php b/app/vendor/cakephp/cakephp/src/ORM/Exception/MissingBehaviorException.php new file mode 100644 index 000000000..725e5e8ac --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Exception/MissingBehaviorException.php @@ -0,0 +1,24 @@ +_entity = $entity; + if (is_array($message)) { + $errors = []; + foreach ($entity->getErrors() as $field => $error) { + $errors[] = $field . ': "' . $this->buildError($error) . '"'; + } + if ($errors) { + $message[] = implode(', ', $errors); + $this->_messageTemplate = 'Entity %s failure (%s).'; + } + } + parent::__construct($message, $code, $previous); + } + + /** + * @param string|array $error Error message. + * @return string + */ + protected function buildError($error) + { + if (!is_array($error)) { + return $error; + } + + $errors = []; + foreach ($error as $key => $message) { + $errors[] = is_int($key) ? $message : $key; + } + + return implode(', ', $errors); + } + + /** + * Get the passed in entity + * + * @return \Cake\Datasource\EntityInterface + */ + public function getEntity() + { + return $this->_entity; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Exception/RolledbackTransactionException.php b/app/vendor/cakephp/cakephp/src/ORM/Exception/RolledbackTransactionException.php new file mode 100644 index 000000000..ac31a9d0f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Exception/RolledbackTransactionException.php @@ -0,0 +1,24 @@ +_getQuery($entities, $contain, $source); + $associations = array_keys($query->getContain()); + + $entities = $this->_injectResults($entities, $query, $associations, $source); + + return $returnSingle ? array_shift($entities) : $entities; + } + + /** + * Builds a query for loading the passed list of entity objects along with the + * associations specified in $contain. + * + * @param \Cake\Collection\CollectionInterface $objects The original entities + * @param array $contain The associations to be loaded + * @param \Cake\ORM\Table $source The table to use for fetching the top level entities + * @return \Cake\ORM\Query + */ + protected function _getQuery($objects, $contain, $source) + { + $primaryKey = $source->getPrimaryKey(); + $method = is_string($primaryKey) ? 'get' : 'extract'; + + $keys = $objects->map(function ($entity) use ($primaryKey, $method) { + return $entity->{$method}($primaryKey); + }); + + /** @var \Cake\ORM\Query $query */ + $query = $source + ->find() + ->select((array)$primaryKey) + ->where(function ($exp, $q) use ($primaryKey, $keys, $source) { + if (is_array($primaryKey) && count($primaryKey) === 1) { + $primaryKey = current($primaryKey); + } + + if (is_string($primaryKey)) { + return $exp->in($source->aliasField($primaryKey), $keys->toList()); + } + + $types = array_intersect_key($q->getDefaultTypes(), array_flip($primaryKey)); + $primaryKey = array_map([$source, 'aliasField'], $primaryKey); + + return new TupleComparison($primaryKey, $keys->toList(), $types, 'IN'); + }) + ->contain($contain); + + foreach ($query->getEagerLoader()->attachableAssociations($source) as $loadable) { + $config = $loadable->getConfig(); + $config['includeFields'] = true; + $loadable->setConfig($config); + } + + return $query; + } + + /** + * Returns a map of property names where the association results should be injected + * in the top level entities. + * + * @param \Cake\ORM\Table $source The table having the top level associations + * @param array $associations The name of the top level associations + * @return array + */ + protected function _getPropertyMap($source, $associations) + { + $map = []; + $container = $source->associations(); + foreach ($associations as $assoc) { + $map[$assoc] = $container->get($assoc)->getProperty(); + } + + return $map; + } + + /** + * Injects the results of the eager loader query into the original list of + * entities. + * + * @param array|\Traversable $objects The original list of entities + * @param \Cake\Collection\CollectionInterface|\Cake\Database\Query $results The loaded results + * @param array $associations The top level associations that were loaded + * @param \Cake\ORM\Table $source The table where the entities came from + * @return array + */ + protected function _injectResults($objects, $results, $associations, $source) + { + $injected = []; + $properties = $this->_getPropertyMap($source, $associations); + $primaryKey = (array)$source->getPrimaryKey(); + $results = $results + ->indexBy(function ($e) use ($primaryKey) { + return implode(';', $e->extract($primaryKey)); + }) + ->toArray(); + + foreach ($objects as $k => $object) { + $key = implode(';', $object->extract($primaryKey)); + if (!isset($results[$key])) { + $injected[$k] = $object; + continue; + } + + $loaded = $results[$key]; + foreach ($associations as $assoc) { + $property = $properties[$assoc]; + $object->set($property, $loaded->get($property), ['useSetters' => false]); + $object->setDirty($property, false); + } + $injected[$k] = $object; + } + + return $injected; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorAwareTrait.php b/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorAwareTrait.php new file mode 100644 index 000000000..2d26e5199 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorAwareTrait.php @@ -0,0 +1,79 @@ +setTableLocator($tableLocator); + } + + return $this->getTableLocator(); + } + + /** + * Sets the table locator. + * + * @param \Cake\ORM\Locator\LocatorInterface $tableLocator LocatorInterface instance. + * @return $this + */ + public function setTableLocator(LocatorInterface $tableLocator) + { + $this->_tableLocator = $tableLocator; + + return $this; + } + + /** + * Gets the table locator. + * + * @return \Cake\ORM\Locator\LocatorInterface + */ + public function getTableLocator() + { + if (!$this->_tableLocator) { + $this->_tableLocator = TableRegistry::getTableLocator(); + } + + return $this->_tableLocator; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorInterface.php b/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorInterface.php new file mode 100644 index 000000000..08cddd6a2 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorInterface.php @@ -0,0 +1,78 @@ +_config = $alias; + + return $this; + } + + if (isset($this->_instances[$alias])) { + throw new RuntimeException(sprintf( + 'You cannot configure "%s", it has already been constructed.', + $alias + )); + } + + $this->_config[$alias] = $options; + + return $this; + } + + /** + * Returns configuration for an alias or the full configuration array for all aliases. + * + * @param string|null $alias Alias to get config for, null for complete config. + * @return array The config data. + */ + public function getConfig($alias = null) + { + if ($alias === null) { + return $this->_config; + } + + return isset($this->_config[$alias]) ? $this->_config[$alias] : []; + } + + /** + * Stores a list of options to be used when instantiating an object + * with a matching alias. + * + * The options that can be stored are those that are recognized by `get()` + * If second argument is omitted, it will return the current settings + * for $alias. + * + * If no arguments are passed it will return the full configuration array for + * all aliases + * + * @deprecated 3.4.0 Use setConfig()/getConfig() instead. + * @param string|array|null $alias Name of the alias + * @param array|null $options list of options for the alias + * @return array The config data. + * @throws \RuntimeException When you attempt to configure an existing table instance. + */ + public function config($alias = null, $options = null) + { + deprecationWarning( + 'TableLocator::config() is deprecated. ' . + 'Use getConfig()/setConfig() instead.' + ); + if ($alias !== null) { + if (is_string($alias) && $options === null) { + return $this->getConfig($alias); + } + + $this->setConfig($alias, $options); + } + + return $this->getConfig($alias); + } + + /** + * Get a table instance from the registry. + * + * Tables are only created once until the registry is flushed. + * This means that aliases must be unique across your application. + * This is important because table associations are resolved at runtime + * and cyclic references need to be handled correctly. + * + * The options that can be passed are the same as in Cake\ORM\Table::__construct(), but the + * `className` key is also recognized. + * + * ### Options + * + * - `className` Define the specific class name to use. If undefined, CakePHP will generate the + * class name based on the alias. For example 'Users' would result in + * `App\Model\Table\UsersTable` being used. If this class does not exist, + * then the default `Cake\ORM\Table` class will be used. By setting the `className` + * option you can define the specific class to use. The className option supports + * plugin short class references {@link Cake\Core\App::shortName()}. + * - `table` Define the table name to use. If undefined, this option will default to the underscored + * version of the alias name. + * - `connection` Inject the specific connection object to use. If this option and `connectionName` are undefined, + * The table class' `defaultConnectionName()` method will be invoked to fetch the connection name. + * - `connectionName` Define the connection name to use. The named connection will be fetched from + * Cake\Datasource\ConnectionManager. + * + * *Note* If your `$alias` uses plugin syntax only the name part will be used as + * key in the registry. This means that if two plugins, or a plugin and app provide + * the same alias, the registry will only store the first instance. + * + * @param string $alias The alias name you want to get. + * @param array $options The options you want to build the table with. + * If a table has already been loaded the options will be ignored. + * @return \Cake\ORM\Table + * @throws \RuntimeException When you try to configure an alias that already exists. + */ + public function get($alias, array $options = []) + { + if (isset($this->_instances[$alias])) { + if (!empty($options) && $this->_options[$alias] !== $options) { + throw new RuntimeException(sprintf( + 'You cannot configure "%s", it already exists in the registry.', + $alias + )); + } + + return $this->_instances[$alias]; + } + + $this->_options[$alias] = $options; + list(, $classAlias) = pluginSplit($alias); + $options = ['alias' => $classAlias] + $options; + + if (isset($this->_config[$alias])) { + $options += $this->_config[$alias]; + } + + $className = $this->_getClassName($alias, $options); + if ($className) { + $options['className'] = $className; + } else { + if (empty($options['className'])) { + $options['className'] = Inflector::camelize($alias); + } + if (!isset($options['table']) && strpos($options['className'], '\\') === false) { + list(, $table) = pluginSplit($options['className']); + $options['table'] = Inflector::underscore($table); + } + $options['className'] = 'Cake\ORM\Table'; + } + + if (empty($options['connection'])) { + if (!empty($options['connectionName'])) { + $connectionName = $options['connectionName']; + } else { + /* @var \Cake\ORM\Table $className */ + $className = $options['className']; + $connectionName = $className::defaultConnectionName(); + } + $options['connection'] = ConnectionManager::get($connectionName); + } + if (empty($options['associations'])) { + $associations = new AssociationCollection($this); + $options['associations'] = $associations; + } + + $options['registryAlias'] = $alias; + $this->_instances[$alias] = $this->_create($options); + + if ($options['className'] === 'Cake\ORM\Table') { + $this->_fallbacked[$alias] = $this->_instances[$alias]; + } + + return $this->_instances[$alias]; + } + + /** + * Gets the table class name. + * + * @param string $alias The alias name you want to get. + * @param array $options Table options array. + * @return string|false + */ + protected function _getClassName($alias, array $options = []) + { + if (empty($options['className'])) { + $options['className'] = Inflector::camelize($alias); + } + + return App::className($options['className'], 'Model/Table', 'Table'); + } + + /** + * Wrapper for creating table instances + * + * @param array $options The alias to check for. + * @return \Cake\ORM\Table + */ + protected function _create(array $options) + { + return new $options['className']($options); + } + + /** + * {@inheritDoc} + */ + public function exists($alias) + { + return isset($this->_instances[$alias]); + } + + /** + * {@inheritDoc} + */ + public function set($alias, Table $object) + { + return $this->_instances[$alias] = $object; + } + + /** + * {@inheritDoc} + */ + public function clear() + { + $this->_instances = []; + $this->_config = []; + $this->_fallbacked = []; + } + + /** + * Returns the list of tables that were created by this registry that could + * not be instantiated from a specific subclass. This method is useful for + * debugging common mistakes when setting up associations or created new table + * classes. + * + * @return \Cake\ORM\Table[] + */ + public function genericInstances() + { + return $this->_fallbacked; + } + + /** + * {@inheritDoc} + */ + public function remove($alias) + { + unset( + $this->_instances[$alias], + $this->_config[$alias], + $this->_fallbacked[$alias] + ); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Marshaller.php b/app/vendor/cakephp/cakephp/src/ORM/Marshaller.php new file mode 100644 index 000000000..7eb255187 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Marshaller.php @@ -0,0 +1,850 @@ +_table = $table; + } + + /** + * Build the map of property => marshalling callable. + * + * @param array $data The data being marshalled. + * @param array $options List of options containing the 'associated' key. + * @throws \InvalidArgumentException When associations do not exist. + * @return array + */ + protected function _buildPropertyMap($data, $options) + { + $map = []; + $schema = $this->_table->getSchema(); + + // Is a concrete column? + foreach (array_keys($data) as $prop) { + $columnType = $schema->getColumnType($prop); + if ($columnType) { + $map[$prop] = function ($value, $entity) use ($columnType) { + return Type::build($columnType)->marshal($value); + }; + } + } + + // Map associations + if (!isset($options['associated'])) { + $options['associated'] = []; + } + $include = $this->_normalizeAssociations($options['associated']); + foreach ($include as $key => $nested) { + if (is_int($key) && is_scalar($nested)) { + $key = $nested; + $nested = []; + } + // If the key is not a special field like _ids or _joinData + // it is a missing association that we should error on. + if (!$this->_table->hasAssociation($key)) { + if (substr($key, 0, 1) !== '_') { + throw new \InvalidArgumentException(sprintf( + 'Cannot marshal data for "%s" association. It is not associated with "%s".', + $key, + $this->_table->getAlias() + )); + } + continue; + } + $assoc = $this->_table->getAssociation($key); + + if (isset($options['forceNew'])) { + $nested['forceNew'] = $options['forceNew']; + } + if (isset($options['isMerge'])) { + $callback = function ($value, $entity) use ($assoc, $nested) { + /** @var \Cake\Datasource\EntityInterface $entity */ + $options = $nested + ['associated' => [], 'association' => $assoc]; + + return $this->_mergeAssociation($entity->get($assoc->getProperty()), $assoc, $value, $options); + }; + } else { + $callback = function ($value, $entity) use ($assoc, $nested) { + $options = $nested + ['associated' => []]; + + return $this->_marshalAssociation($assoc, $value, $options); + }; + } + $map[$assoc->getProperty()] = $callback; + } + + $behaviors = $this->_table->behaviors(); + foreach ($behaviors->loaded() as $name) { + $behavior = $behaviors->get($name); + if ($behavior instanceof PropertyMarshalInterface) { + $map += $behavior->buildMarshalMap($this, $map, $options); + } + } + + return $map; + } + + /** + * Hydrate one entity and its associated data. + * + * ### Options: + * + * - validate: Set to false to disable validation. Can also be a string of the validator ruleset to be applied. + * Defaults to true/default. + * - associated: Associations listed here will be marshalled as well. Defaults to null. + * - fieldList: (deprecated) Since 3.4.0. Use fields instead. + * - fields: A whitelist of fields to be assigned to the entity. If not present, + * the accessible fields list in the entity will be used. Defaults to null. + * - accessibleFields: A list of fields to allow or deny in entity accessible fields. Defaults to null + * - forceNew: When enabled, belongsToMany associations will have 'new' entities created + * when primary key values are set, and a record does not already exist. Normally primary key + * on missing entities would be ignored. Defaults to false. + * + * The above options can be used in each nested `associated` array. In addition to the above + * options you can also use the `onlyIds` option for HasMany and BelongsToMany associations. + * When true this option restricts the request data to only be read from `_ids`. + * + * ``` + * $result = $marshaller->one($data, [ + * 'associated' => ['Tags' => ['onlyIds' => true]] + * ]); + * ``` + * + * @param array $data The data to hydrate. + * @param array $options List of options + * @return \Cake\Datasource\EntityInterface + * @see \Cake\ORM\Table::newEntity() + * @see \Cake\ORM\Entity::$_accessible + */ + public function one(array $data, array $options = []) + { + list($data, $options) = $this->_prepareDataAndOptions($data, $options); + + $primaryKey = (array)$this->_table->getPrimaryKey(); + $entityClass = $this->_table->getEntityClass(); + /** @var \Cake\Datasource\EntityInterface $entity */ + $entity = new $entityClass(); + $entity->setSource($this->_table->getRegistryAlias()); + + if (isset($options['accessibleFields'])) { + foreach ((array)$options['accessibleFields'] as $key => $value) { + $entity->setAccess($key, $value); + } + } + $errors = $this->_validate($data, $options, true); + + $options['isMerge'] = false; + $propertyMap = $this->_buildPropertyMap($data, $options); + $properties = []; + foreach ($data as $key => $value) { + if (!empty($errors[$key])) { + if ($entity instanceof InvalidPropertyInterface) { + $entity->setInvalidField($key, $value); + } + continue; + } + + if ($value === '' && in_array($key, $primaryKey, true)) { + // Skip marshalling '' for pk fields. + continue; + } + if (isset($propertyMap[$key])) { + $properties[$key] = $propertyMap[$key]($value, $entity); + } else { + $properties[$key] = $value; + } + } + + if (isset($options['fields'])) { + foreach ((array)$options['fields'] as $field) { + if (array_key_exists($field, $properties)) { + $entity->set($field, $properties[$field]); + } + } + } else { + $entity->set($properties); + } + + // Don't flag clean association entities as + // dirty so we don't persist empty records. + foreach ($properties as $field => $value) { + if ($value instanceof EntityInterface) { + $entity->setDirty($field, $value->isDirty()); + } + } + + $entity->setErrors($errors); + + return $entity; + } + + /** + * Returns the validation errors for a data set based on the passed options + * + * @param array $data The data to validate. + * @param array $options The options passed to this marshaller. + * @param bool $isNew Whether it is a new entity or one to be updated. + * @return array The list of validation errors. + * @throws \RuntimeException If no validator can be created. + */ + protected function _validate($data, $options, $isNew) + { + if (!$options['validate']) { + return []; + } + + $validator = null; + if ($options['validate'] === true) { + $validator = $this->_table->getValidator(); + } elseif (is_string($options['validate'])) { + $validator = $this->_table->getValidator($options['validate']); + } elseif (is_object($options['validate'])) { + $validator = $options['validate']; + } + + if ($validator === null) { + throw new RuntimeException( + sprintf('validate must be a boolean, a string or an object. Got %s.', getTypeName($options['validate'])) + ); + } + + return $validator->errors($data, $isNew); + } + + /** + * Returns data and options prepared to validate and marshall. + * + * @param array $data The data to prepare. + * @param array $options The options passed to this marshaller. + * @return array An array containing prepared data and options. + */ + protected function _prepareDataAndOptions($data, $options) + { + $options += ['validate' => true]; + + if (!isset($options['fields']) && isset($options['fieldList'])) { + deprecationWarning( + 'The `fieldList` option for marshalling is deprecated. Use the `fields` option instead.' + ); + $options['fields'] = $options['fieldList']; + unset($options['fieldList']); + } + + $tableName = $this->_table->getAlias(); + if (isset($data[$tableName])) { + $data += $data[$tableName]; + unset($data[$tableName]); + } + + $data = new ArrayObject($data); + $options = new ArrayObject($options); + $this->_table->dispatchEvent('Model.beforeMarshal', compact('data', 'options')); + + return [(array)$data, (array)$options]; + } + + /** + * Create a new sub-marshaller and marshal the associated data. + * + * @param \Cake\ORM\Association $assoc The association to marshall + * @param array $value The data to hydrate + * @param array $options List of options. + * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|null + */ + protected function _marshalAssociation($assoc, $value, $options) + { + if (!is_array($value)) { + return null; + } + $targetTable = $assoc->getTarget(); + $marshaller = $targetTable->marshaller(); + $types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE]; + if (in_array($assoc->type(), $types)) { + return $marshaller->one($value, (array)$options); + } + if ($assoc->type() === Association::ONE_TO_MANY || $assoc->type() === Association::MANY_TO_MANY) { + $hasIds = array_key_exists('_ids', $value); + $onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds']; + + if ($hasIds && is_array($value['_ids'])) { + return $this->_loadAssociatedByIds($assoc, $value['_ids']); + } + if ($hasIds || $onlyIds) { + return []; + } + } + if ($assoc->type() === Association::MANY_TO_MANY) { + return $marshaller->_belongsToMany($assoc, $value, (array)$options); + } + + return $marshaller->many($value, (array)$options); + } + + /** + * Hydrate many entities and their associated data. + * + * ### Options: + * + * - validate: Set to false to disable validation. Can also be a string of the validator ruleset to be applied. + * Defaults to true/default. + * - associated: Associations listed here will be marshalled as well. Defaults to null. + * - fieldList: (deprecated) Since 3.4.0. Use fields instead + * - fields: A whitelist of fields to be assigned to the entity. If not present, + * the accessible fields list in the entity will be used. Defaults to null. + * - accessibleFields: A list of fields to allow or deny in entity accessible fields. Defaults to null + * - forceNew: When enabled, belongsToMany associations will have 'new' entities created + * when primary key values are set, and a record does not already exist. Normally primary key + * on missing entities would be ignored. Defaults to false. + * + * @param array $data The data to hydrate. + * @param array $options List of options + * @return \Cake\Datasource\EntityInterface[] An array of hydrated records. + * @see \Cake\ORM\Table::newEntities() + * @see \Cake\ORM\Entity::$_accessible + */ + public function many(array $data, array $options = []) + { + $output = []; + foreach ($data as $record) { + if (!is_array($record)) { + continue; + } + $output[] = $this->one($record, $options); + } + + return $output; + } + + /** + * Marshals data for belongsToMany associations. + * + * Builds the related entities and handles the special casing + * for junction table entities. + * + * @param \Cake\ORM\Association\BelongsToMany $assoc The association to marshal. + * @param array $data The data to convert into entities. + * @param array $options List of options. + * @return \Cake\Datasource\EntityInterface[] An array of built entities. + * @throws \BadMethodCallException + * @throws \InvalidArgumentException + * @throws \RuntimeException + */ + protected function _belongsToMany(BelongsToMany $assoc, array $data, $options = []) + { + $associated = isset($options['associated']) ? $options['associated'] : []; + $forceNew = isset($options['forceNew']) ? $options['forceNew'] : false; + + $data = array_values($data); + + $target = $assoc->getTarget(); + $primaryKey = array_flip((array)$target->getPrimaryKey()); + $records = $conditions = []; + $primaryCount = count($primaryKey); + $conditions = []; + + foreach ($data as $i => $row) { + if (!is_array($row)) { + continue; + } + if (array_intersect_key($primaryKey, $row) === $primaryKey) { + $keys = array_intersect_key($row, $primaryKey); + if (count($keys) === $primaryCount) { + $rowConditions = []; + foreach ($keys as $key => $value) { + $rowConditions[][$target->aliasField($key)] = $value; + } + + if ($forceNew && !$target->exists($rowConditions)) { + $records[$i] = $this->one($row, $options); + } + + $conditions = array_merge($conditions, $rowConditions); + } + } else { + $records[$i] = $this->one($row, $options); + } + } + + if (!empty($conditions)) { + $query = $target->find(); + $query->andWhere(function ($exp) use ($conditions) { + /** @var \Cake\Database\Expression\QueryExpression $exp */ + return $exp->or_($conditions); + }); + + $keyFields = array_keys($primaryKey); + + $existing = []; + foreach ($query as $row) { + $k = implode(';', $row->extract($keyFields)); + $existing[$k] = $row; + } + + foreach ($data as $i => $row) { + $key = []; + foreach ($keyFields as $k) { + if (isset($row[$k])) { + $key[] = $row[$k]; + } + } + $key = implode(';', $key); + + // Update existing record and child associations + if (isset($existing[$key])) { + $records[$i] = $this->merge($existing[$key], $data[$i], $options); + } + } + } + + $jointMarshaller = $assoc->junction()->marshaller(); + + $nested = []; + if (isset($associated['_joinData'])) { + $nested = (array)$associated['_joinData']; + } + + foreach ($records as $i => $record) { + // Update junction table data in _joinData. + if (isset($data[$i]['_joinData'])) { + $joinData = $jointMarshaller->one($data[$i]['_joinData'], $nested); + $record->set('_joinData', $joinData); + } + } + + return $records; + } + + /** + * Loads a list of belongs to many from ids. + * + * @param \Cake\ORM\Association $assoc The association class for the belongsToMany association. + * @param array $ids The list of ids to load. + * @return \Cake\Datasource\EntityInterface[] An array of entities. + */ + protected function _loadAssociatedByIds($assoc, $ids) + { + if (empty($ids)) { + return []; + } + + $target = $assoc->getTarget(); + $primaryKey = (array)$target->getPrimaryKey(); + $multi = count($primaryKey) > 1; + $primaryKey = array_map([$target, 'aliasField'], $primaryKey); + + if ($multi) { + $first = current($ids); + if (!is_array($first) || count($first) !== count($primaryKey)) { + return []; + } + $filter = new TupleComparison($primaryKey, $ids, [], 'IN'); + } else { + $filter = [$primaryKey[0] . ' IN' => $ids]; + } + + return $target->find()->where($filter)->toArray(); + } + + /** + * Loads a list of belongs to many from ids. + * + * @param \Cake\ORM\Association $assoc The association class for the belongsToMany association. + * @param array $ids The list of ids to load. + * @return \Cake\Datasource\EntityInterface[] An array of entities. + * @deprecated Use _loadAssociatedByIds() + */ + protected function _loadBelongsToMany($assoc, $ids) + { + deprecationWarning( + 'Marshaller::_loadBelongsToMany() is deprecated. Use _loadAssociatedByIds() instead.' + ); + + return $this->_loadAssociatedByIds($assoc, $ids); + } + + /** + * Merges `$data` into `$entity` and recursively does the same for each one of + * the association names passed in `$options`. When merging associations, if an + * entity is not present in the parent entity for a given association, a new one + * will be created. + * + * When merging HasMany or BelongsToMany associations, all the entities in the + * `$data` array will appear, those that can be matched by primary key will get + * the data merged, but those that cannot, will be discarded. `ids` option can be used + * to determine whether the association must use the `_ids` format. + * + * ### Options: + * + * - associated: Associations listed here will be marshalled as well. + * - validate: Whether or not to validate data before hydrating the entities. Can + * also be set to a string to use a specific validator. Defaults to true/default. + * - fieldList: (deprecated) Since 3.4.0. Use fields instead + * - fields: A whitelist of fields to be assigned to the entity. If not present + * the accessible fields list in the entity will be used. + * - accessibleFields: A list of fields to allow or deny in entity accessible fields. + * + * The above options can be used in each nested `associated` array. In addition to the above + * options you can also use the `onlyIds` option for HasMany and BelongsToMany associations. + * When true this option restricts the request data to only be read from `_ids`. + * + * ``` + * $result = $marshaller->merge($entity, $data, [ + * 'associated' => ['Tags' => ['onlyIds' => true]] + * ]); + * ``` + * + * @param \Cake\Datasource\EntityInterface $entity the entity that will get the + * data merged in + * @param array $data key value list of fields to be merged into the entity + * @param array $options List of options. + * @return \Cake\Datasource\EntityInterface + * @see \Cake\ORM\Entity::$_accessible + */ + public function merge(EntityInterface $entity, array $data, array $options = []) + { + list($data, $options) = $this->_prepareDataAndOptions($data, $options); + + $isNew = $entity->isNew(); + $keys = []; + + if (!$isNew) { + $keys = $entity->extract((array)$this->_table->getPrimaryKey()); + } + + if (isset($options['accessibleFields'])) { + foreach ((array)$options['accessibleFields'] as $key => $value) { + $entity->setAccess($key, $value); + } + } + + $errors = $this->_validate($data + $keys, $options, $isNew); + $options['isMerge'] = true; + $propertyMap = $this->_buildPropertyMap($data, $options); + $properties = []; + foreach ($data as $key => $value) { + if (!empty($errors[$key])) { + if ($entity instanceof InvalidPropertyInterface) { + $entity->setInvalidField($key, $value); + } + continue; + } + $original = $entity->get($key); + + if (isset($propertyMap[$key])) { + $value = $propertyMap[$key]($value, $entity); + + // Don't dirty scalar values and objects that didn't + // change. Arrays will always be marked as dirty because + // the original/updated list could contain references to the + // same objects, even though those objects may have changed internally. + if ((is_scalar($value) && $original === $value) || + ($value === null && $original === $value) || + (is_object($value) && !($value instanceof EntityInterface) && $original == $value) + ) { + continue; + } + } + $properties[$key] = $value; + } + + $entity->setErrors($errors); + if (!isset($options['fields'])) { + $entity->set($properties); + + foreach ($properties as $field => $value) { + if ($value instanceof EntityInterface) { + $entity->setDirty($field, $value->isDirty()); + } + } + + return $entity; + } + + foreach ((array)$options['fields'] as $field) { + if (!array_key_exists($field, $properties)) { + continue; + } + $entity->set($field, $properties[$field]); + if ($properties[$field] instanceof EntityInterface) { + $entity->setDirty($field, $properties[$field]->isDirty()); + } + } + + return $entity; + } + + /** + * Merges each of the elements from `$data` into each of the entities in `$entities` + * and recursively does the same for each of the association names passed in + * `$options`. When merging associations, if an entity is not present in the parent + * entity for a given association, a new one will be created. + * + * Records in `$data` are matched against the entities using the primary key + * column. Entries in `$entities` that cannot be matched to any record in + * `$data` will be discarded. Records in `$data` that could not be matched will + * be marshalled as a new entity. + * + * When merging HasMany or BelongsToMany associations, all the entities in the + * `$data` array will appear, those that can be matched by primary key will get + * the data merged, but those that cannot, will be discarded. + * + * ### Options: + * + * - validate: Whether or not to validate data before hydrating the entities. Can + * also be set to a string to use a specific validator. Defaults to true/default. + * - associated: Associations listed here will be marshalled as well. + * - fieldList: (deprecated) Since 3.4.0. Use fields instead + * - fields: A whitelist of fields to be assigned to the entity. If not present, + * the accessible fields list in the entity will be used. + * - accessibleFields: A list of fields to allow or deny in entity accessible fields. + * + * @param \Cake\Datasource\EntityInterface[]|\Traversable $entities the entities that will get the + * data merged in + * @param array $data list of arrays to be merged into the entities + * @param array $options List of options. + * @return \Cake\Datasource\EntityInterface[] + * @see \Cake\ORM\Entity::$_accessible + */ + public function mergeMany($entities, array $data, array $options = []) + { + $primary = (array)$this->_table->getPrimaryKey(); + + $indexed = (new Collection($data)) + ->groupBy(function ($el) use ($primary) { + $keys = []; + foreach ($primary as $key) { + $keys[] = isset($el[$key]) ? $el[$key] : ''; + } + + return implode(';', $keys); + }) + ->map(function ($element, $key) { + return $key === '' ? $element : $element[0]; + }) + ->toArray(); + + $new = isset($indexed[null]) ? $indexed[null] : []; + unset($indexed[null]); + $output = []; + + foreach ($entities as $entity) { + if (!($entity instanceof EntityInterface)) { + continue; + } + + $key = implode(';', $entity->extract($primary)); + if ($key === null || !isset($indexed[$key])) { + continue; + } + + $output[] = $this->merge($entity, $indexed[$key], $options); + unset($indexed[$key]); + } + + $conditions = (new Collection($indexed)) + ->map(function ($data, $key) { + return explode(';', $key); + }) + ->filter(function ($keys) use ($primary) { + return count(array_filter($keys, 'strlen')) === count($primary); + }) + ->reduce(function ($conditions, $keys) use ($primary) { + $fields = array_map([$this->_table, 'aliasField'], $primary); + $conditions['OR'][] = array_combine($fields, $keys); + + return $conditions; + }, ['OR' => []]); + $maybeExistentQuery = $this->_table->find()->where($conditions); + + if (!empty($indexed) && count($maybeExistentQuery->clause('where'))) { + foreach ($maybeExistentQuery as $entity) { + $key = implode(';', $entity->extract($primary)); + if (isset($indexed[$key])) { + $output[] = $this->merge($entity, $indexed[$key], $options); + unset($indexed[$key]); + } + } + } + + foreach ((new Collection($indexed))->append($new) as $value) { + if (!is_array($value)) { + continue; + } + $output[] = $this->one($value, $options); + } + + return $output; + } + + /** + * Creates a new sub-marshaller and merges the associated data. + * + * @param \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[] $original The original entity + * @param \Cake\ORM\Association $assoc The association to merge + * @param array $value The data to hydrate + * @param array $options List of options. + * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|null + */ + protected function _mergeAssociation($original, $assoc, $value, $options) + { + if (!$original) { + return $this->_marshalAssociation($assoc, $value, $options); + } + if (!is_array($value)) { + return null; + } + + $targetTable = $assoc->getTarget(); + $marshaller = $targetTable->marshaller(); + $types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE]; + if (in_array($assoc->type(), $types)) { + return $marshaller->merge($original, $value, (array)$options); + } + if ($assoc->type() === Association::MANY_TO_MANY) { + return $marshaller->_mergeBelongsToMany($original, $assoc, $value, (array)$options); + } + + return $marshaller->mergeMany($original, $value, (array)$options); + } + + /** + * Creates a new sub-marshaller and merges the associated data for a BelongstoMany + * association. + * + * @param \Cake\Datasource\EntityInterface $original The original entity + * @param \Cake\ORM\Association $assoc The association to marshall + * @param array $value The data to hydrate + * @param array $options List of options. + * @return \Cake\Datasource\EntityInterface[] + */ + protected function _mergeBelongsToMany($original, $assoc, $value, $options) + { + $associated = isset($options['associated']) ? $options['associated'] : []; + + $hasIds = array_key_exists('_ids', $value); + $onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds']; + + if ($hasIds && is_array($value['_ids'])) { + return $this->_loadAssociatedByIds($assoc, $value['_ids']); + } + if ($hasIds || $onlyIds) { + return []; + } + + if (!empty($associated) && !in_array('_joinData', $associated) && !isset($associated['_joinData'])) { + return $this->mergeMany($original, $value, $options); + } + + return $this->_mergeJoinData($original, $assoc, $value, $options); + } + + /** + * Merge the special _joinData property into the entity set. + * + * @param \Cake\Datasource\EntityInterface $original The original entity + * @param \Cake\ORM\Association\BelongsToMany $assoc The association to marshall + * @param array $value The data to hydrate + * @param array $options List of options. + * @return \Cake\Datasource\EntityInterface[] An array of entities + */ + protected function _mergeJoinData($original, $assoc, $value, $options) + { + $associated = isset($options['associated']) ? $options['associated'] : []; + $extra = []; + foreach ($original as $entity) { + // Mark joinData as accessible so we can marshal it properly. + $entity->setAccess('_joinData', true); + + $joinData = $entity->get('_joinData'); + if ($joinData && $joinData instanceof EntityInterface) { + $extra[spl_object_hash($entity)] = $joinData; + } + } + + $joint = $assoc->junction(); + $marshaller = $joint->marshaller(); + + $nested = []; + if (isset($associated['_joinData'])) { + $nested = (array)$associated['_joinData']; + } + + $options['accessibleFields'] = ['_joinData' => true]; + + $records = $this->mergeMany($original, $value, $options); + foreach ($records as $record) { + $hash = spl_object_hash($record); + $value = $record->get('_joinData'); + + // Already an entity, no further marshalling required. + if ($value instanceof EntityInterface) { + continue; + } + + // Scalar data can't be handled + if (!is_array($value)) { + $record->unsetProperty('_joinData'); + continue; + } + + // Marshal data into the old object, or make a new joinData object. + if (isset($extra[$hash])) { + $record->set('_joinData', $marshaller->merge($extra[$hash], $value, $nested)); + } elseif (is_array($value)) { + $joinData = $marshaller->one($value, $nested); + $record->set('_joinData', $joinData); + } + } + + return $records; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/PropertyMarshalInterface.php b/app/vendor/cakephp/cakephp/src/ORM/PropertyMarshalInterface.php new file mode 100644 index 000000000..bf2c6d565 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/PropertyMarshalInterface.php @@ -0,0 +1,34 @@ + callable]` of additional properties to marshal. + */ + public function buildMarshalMap($marshaller, $map, $options); +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Query.php b/app/vendor/cakephp/cakephp/src/ORM/Query.php new file mode 100644 index 000000000..746f25789 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Query.php @@ -0,0 +1,1396 @@ +repository($table); + + if ($this->_repository) { + $this->addDefaultTypes($this->_repository); + } + } + + /** + * Adds new fields to be returned by a `SELECT` statement when this query is + * executed. Fields can be passed as an array of strings, array of expression + * objects, a single expression or a single string. + * + * If an array is passed, keys will be used to alias fields using the value as the + * real field to be aliased. It is possible to alias strings, Expression objects or + * even other Query objects. + * + * If a callable function is passed, the returning array of the function will + * be used as the list of fields. + * + * By default this function will append any passed argument to the list of fields + * to be selected, unless the second argument is set to true. + * + * ### Examples: + * + * ``` + * $query->select(['id', 'title']); // Produces SELECT id, title + * $query->select(['author' => 'author_id']); // Appends author: SELECT id, title, author_id as author + * $query->select('id', true); // Resets the list: SELECT id + * $query->select(['total' => $countQuery]); // SELECT id, (SELECT ...) AS total + * $query->select(function ($query) { + * return ['article_id', 'total' => $query->count('*')]; + * }) + * ``` + * + * By default no fields are selected, if you have an instance of `Cake\ORM\Query` and try to append + * fields you should also call `Cake\ORM\Query::enableAutoFields()` to select the default fields + * from the table. + * + * If you pass an instance of a `Cake\ORM\Table` or `Cake\ORM\Association` class, + * all the fields in the schema of the table or the association will be added to + * the select clause. + * + * @param array|\Cake\Database\ExpressionInterface|string|\Cake\ORM\Table|\Cake\ORM\Association $fields fields + * to be added to the list. + * @param bool $overwrite whether to reset fields with passed list or not + * @return $this + */ + public function select($fields = [], $overwrite = false) + { + if ($fields instanceof Association) { + $fields = $fields->getTarget(); + } + + if ($fields instanceof Table) { + $fields = $this->aliasFields($fields->getSchema()->columns(), $fields->getAlias()); + } + + return parent::select($fields, $overwrite); + } + + /** + * All the fields associated with the passed table except the excluded + * fields will be added to the select clause of the query. Passed excluded fields should not be aliased. + * After the first call to this method, a second call cannot be used to remove fields that have already + * been added to the query by the first. If you need to change the list after the first call, + * pass overwrite boolean true which will reset the select clause removing all previous additions. + * + * + * + * @param \Cake\ORM\Table|\Cake\ORM\Association $table The table to use to get an array of columns + * @param array $excludedFields The un-aliased column names you do not want selected from $table + * @param bool $overwrite Whether to reset/remove previous selected fields + * @return Query + * @throws \InvalidArgumentException If Association|Table is not passed in first argument + */ + public function selectAllExcept($table, array $excludedFields, $overwrite = false) + { + if ($table instanceof Association) { + $table = $table->getTarget(); + } + + if (!($table instanceof Table)) { + throw new \InvalidArgumentException('You must provide either an Association or a Table object'); + } + + $fields = array_diff($table->getSchema()->columns(), $excludedFields); + $aliasedFields = $this->aliasFields($fields); + + return $this->select($aliasedFields, $overwrite); + } + + /** + * Hints this object to associate the correct types when casting conditions + * for the database. This is done by extracting the field types from the schema + * associated to the passed table object. This prevents the user from repeating + * themselves when specifying conditions. + * + * This method returns the same query object for chaining. + * + * @param \Cake\ORM\Table $table The table to pull types from + * @return $this + */ + public function addDefaultTypes(Table $table) + { + $alias = $table->getAlias(); + $map = $table->getSchema()->typeMap(); + $fields = []; + foreach ($map as $f => $type) { + $fields[$f] = $fields[$alias . '.' . $f] = $fields[$alias . '__' . $f] = $type; + } + $this->getTypeMap()->addDefaults($fields); + + return $this; + } + + /** + * Sets the instance of the eager loader class to use for loading associations + * and storing containments. + * + * @param \Cake\ORM\EagerLoader $instance The eager loader to use. + * @return $this + */ + public function setEagerLoader(EagerLoader $instance) + { + $this->_eagerLoader = $instance; + + return $this; + } + + /** + * Returns the currently configured instance. + * + * @return \Cake\ORM\EagerLoader + */ + public function getEagerLoader() + { + if ($this->_eagerLoader === null) { + $this->_eagerLoader = new EagerLoader(); + } + + return $this->_eagerLoader; + } + + /** + * Sets the instance of the eager loader class to use for loading associations + * and storing containments. If called with no arguments, it will return the + * currently configured instance. + * + * @deprecated 3.4.0 Use setEagerLoader()/getEagerLoader() instead. + * @param \Cake\ORM\EagerLoader|null $instance The eager loader to use. Pass null + * to get the current eagerloader. + * @return \Cake\ORM\EagerLoader|$this + */ + public function eagerLoader(EagerLoader $instance = null) + { + deprecationWarning( + 'Query::eagerLoader() is deprecated. ' . + 'Use setEagerLoader()/getEagerLoader() instead.' + ); + if ($instance !== null) { + return $this->setEagerLoader($instance); + } + + return $this->getEagerLoader(); + } + + /** + * Sets the list of associations that should be eagerly loaded along with this + * query. The list of associated tables passed must have been previously set as + * associations using the Table API. + * + * ### Example: + * + * ``` + * // Bring articles' author information + * $query->contain('Author'); + * + * // Also bring the category and tags associated to each article + * $query->contain(['Category', 'Tag']); + * ``` + * + * Associations can be arbitrarily nested using dot notation or nested arrays, + * this allows this object to calculate joins or any additional queries that + * must be executed to bring the required associated data. + * + * ### Example: + * + * ``` + * // Eager load the product info, and for each product load other 2 associations + * $query->contain(['Product' => ['Manufacturer', 'Distributor']); + * + * // Which is equivalent to calling + * $query->contain(['Products.Manufactures', 'Products.Distributors']); + * + * // For an author query, load his region, state and country + * $query->contain('Regions.States.Countries'); + * ``` + * + * It is possible to control the conditions and fields selected for each of the + * contained associations: + * + * ### Example: + * + * ``` + * $query->contain(['Tags' => function ($q) { + * return $q->where(['Tags.is_popular' => true]); + * }]); + * + * $query->contain(['Products.Manufactures' => function ($q) { + * return $q->select(['name'])->where(['Manufactures.active' => true]); + * }]); + * ``` + * + * Each association might define special options when eager loaded, the allowed + * options that can be set per association are: + * + * - `foreignKey`: Used to set a different field to match both tables, if set to false + * no join conditions will be generated automatically. `false` can only be used on + * joinable associations and cannot be used with hasMany or belongsToMany associations. + * - `fields`: An array with the fields that should be fetched from the association. + * - `finder`: The finder to use when loading associated records. Either the name of the + * finder as a string, or an array to define options to pass to the finder. + * - `queryBuilder`: Equivalent to passing a callable instead of an options array. + * + * ### Example: + * + * ``` + * // Set options for the hasMany articles that will be eagerly loaded for an author + * $query->contain([ + * 'Articles' => [ + * 'fields' => ['title', 'author_id'] + * ] + * ]); + * ``` + * + * Finders can be configured to use options. + * + * ``` + * // Retrieve translations for the articles, but only those for the `en` and `es` locales + * $query->contain([ + * 'Articles' => [ + * 'finder' => [ + * 'translations' => [ + * 'locales' => ['en', 'es'] + * ] + * ] + * ] + * ]); + * ``` + * + * When containing associations, it is important to include foreign key columns. + * Failing to do so will trigger exceptions. + * + * ``` + * // Use a query builder to add conditions to the containment + * $query->contain('Authors', function ($q) { + * return $q->where(...); // add conditions + * }); + * // Use special join conditions for multiple containments in the same method call + * $query->contain([ + * 'Authors' => [ + * 'foreignKey' => false, + * 'queryBuilder' => function ($q) { + * return $q->where(...); // Add full filtering conditions + * } + * ], + * 'Tags' => function ($q) { + * return $q->where(...); // add conditions + * } + * ]); + * ``` + * + * If called with no arguments, this function will return an array with + * with the list of previously configured associations to be contained in the + * result. This getter part is deprecated as of 3.6.0. Use getContain() instead. + * + * If called with an empty first argument and `$override` is set to true, the + * previous list will be emptied. + * + * @param array|string|null $associations List of table aliases to be queried. + * @param callable|bool $override The query builder for the association, or + * if associations is an array, a bool on whether to override previous list + * with the one passed + * defaults to merging previous list with the new one. + * @return array|$this + */ + public function contain($associations = null, $override = false) + { + $loader = $this->getEagerLoader(); + if ($override === true) { + $this->clearContain(); + } + + if ($associations === null) { + deprecationWarning( + 'Using Query::contain() as getter is deprecated. ' . + 'Use getContain() instead.' + ); + + return $loader->getContain(); + } + + $queryBuilder = null; + if (is_callable($override)) { + $queryBuilder = $override; + } + + if ($associations) { + $loader->contain($associations, $queryBuilder); + } + $this->_addAssociationsToTypeMap( + $this->getRepository(), + $this->getTypeMap(), + $loader->getContain() + ); + + return $this; + } + + /** + * @return array + */ + public function getContain() + { + return $this->getEagerLoader()->getContain(); + } + + /** + * Clears the contained associations from the current query. + * + * @return $this + */ + public function clearContain() + { + $this->getEagerLoader()->clearContain(); + $this->_dirty(); + + return $this; + } + + /** + * Used to recursively add contained association column types to + * the query. + * + * @param \Cake\ORM\Table $table The table instance to pluck associations from. + * @param \Cake\Database\TypeMap $typeMap The typemap to check for columns in. + * This typemap is indirectly mutated via Cake\ORM\Query::addDefaultTypes() + * @param array $associations The nested tree of associations to walk. + * @return void + */ + protected function _addAssociationsToTypeMap($table, $typeMap, $associations) + { + foreach ($associations as $name => $nested) { + if (!$table->hasAssociation($name)) { + continue; + } + $association = $table->getAssociation($name); + $target = $association->getTarget(); + $primary = (array)$target->getPrimaryKey(); + if (empty($primary) || $typeMap->type($target->aliasField($primary[0])) === null) { + $this->addDefaultTypes($target); + } + if (!empty($nested)) { + $this->_addAssociationsToTypeMap($target, $typeMap, $nested); + } + } + } + + /** + * Adds filtering conditions to this query to only bring rows that have a relation + * to another from an associated table, based on conditions in the associated table. + * + * This function will add entries in the `contain` graph. + * + * ### Example: + * + * ``` + * // Bring only articles that were tagged with 'cake' + * $query->matching('Tags', function ($q) { + * return $q->where(['name' => 'cake']); + * ); + * ``` + * + * It is possible to filter by deep associations by using dot notation: + * + * ### Example: + * + * ``` + * // Bring only articles that were commented by 'markstory' + * $query->matching('Comments.Users', function ($q) { + * return $q->where(['username' => 'markstory']); + * ); + * ``` + * + * As this function will create `INNER JOIN`, you might want to consider + * calling `distinct` on this query as you might get duplicate rows if + * your conditions don't filter them already. This might be the case, for example, + * of the same user commenting more than once in the same article. + * + * ### Example: + * + * ``` + * // Bring unique articles that were commented by 'markstory' + * $query->distinct(['Articles.id']) + * ->matching('Comments.Users', function ($q) { + * return $q->where(['username' => 'markstory']); + * ); + * ``` + * + * Please note that the query passed to the closure will only accept calling + * `select`, `where`, `andWhere` and `orWhere` on it. If you wish to + * add more complex clauses you can do it directly in the main query. + * + * @param string $assoc The association to filter by + * @param callable|null $builder a function that will receive a pre-made query object + * that can be used to add custom conditions or selecting some fields + * @return $this + */ + public function matching($assoc, callable $builder = null) + { + $result = $this->getEagerLoader()->setMatching($assoc, $builder)->getMatching(); + $this->_addAssociationsToTypeMap($this->getRepository(), $this->getTypeMap(), $result); + $this->_dirty(); + + return $this; + } + + /** + * Creates a LEFT JOIN with the passed association table while preserving + * the foreign key matching and the custom conditions that were originally set + * for it. + * + * This function will add entries in the `contain` graph. + * + * ### Example: + * + * ``` + * // Get the count of articles per user + * $usersQuery + * ->select(['total_articles' => $query->func()->count('Articles.id')]) + * ->leftJoinWith('Articles') + * ->group(['Users.id']) + * ->enableAutoFields(true); + * ``` + * + * You can also customize the conditions passed to the LEFT JOIN: + * + * ``` + * // Get the count of articles per user with at least 5 votes + * $usersQuery + * ->select(['total_articles' => $query->func()->count('Articles.id')]) + * ->leftJoinWith('Articles', function ($q) { + * return $q->where(['Articles.votes >=' => 5]); + * }) + * ->group(['Users.id']) + * ->enableAutoFields(true); + * ``` + * + * This will create the following SQL: + * + * ``` + * SELECT COUNT(Articles.id) AS total_articles, Users.* + * FROM users Users + * LEFT JOIN articles Articles ON Articles.user_id = Users.id AND Articles.votes >= 5 + * GROUP BY USers.id + * ``` + * + * It is possible to left join deep associations by using dot notation + * + * ### Example: + * + * ``` + * // Total comments in articles by 'markstory' + * $query + * ->select(['total_comments' => $query->func()->count('Comments.id')]) + * ->leftJoinWith('Comments.Users', function ($q) { + * return $q->where(['username' => 'markstory']); + * ) + * ->group(['Users.id']); + * ``` + * + * Please note that the query passed to the closure will only accept calling + * `select`, `where`, `andWhere` and `orWhere` on it. If you wish to + * add more complex clauses you can do it directly in the main query. + * + * @param string $assoc The association to join with + * @param callable|null $builder a function that will receive a pre-made query object + * that can be used to add custom conditions or selecting some fields + * @return $this + */ + public function leftJoinWith($assoc, callable $builder = null) + { + $result = $this->getEagerLoader() + ->setMatching($assoc, $builder, [ + 'joinType' => QueryInterface::JOIN_TYPE_LEFT, + 'fields' => false + ]) + ->getMatching(); + $this->_addAssociationsToTypeMap($this->getRepository(), $this->getTypeMap(), $result); + $this->_dirty(); + + return $this; + } + + /** + * Creates an INNER JOIN with the passed association table while preserving + * the foreign key matching and the custom conditions that were originally set + * for it. + * + * This function will add entries in the `contain` graph. + * + * ### Example: + * + * ``` + * // Bring only articles that were tagged with 'cake' + * $query->innerJoinWith('Tags', function ($q) { + * return $q->where(['name' => 'cake']); + * ); + * ``` + * + * This will create the following SQL: + * + * ``` + * SELECT Articles.* + * FROM articles Articles + * INNER JOIN tags Tags ON Tags.name = 'cake' + * INNER JOIN articles_tags ArticlesTags ON ArticlesTags.tag_id = Tags.id + * AND ArticlesTags.articles_id = Articles.id + * ``` + * + * This function works the same as `matching()` with the difference that it + * will select no fields from the association. + * + * @param string $assoc The association to join with + * @param callable|null $builder a function that will receive a pre-made query object + * that can be used to add custom conditions or selecting some fields + * @return $this + * @see \Cake\ORM\Query::matching() + */ + public function innerJoinWith($assoc, callable $builder = null) + { + $result = $this->getEagerLoader() + ->setMatching($assoc, $builder, [ + 'joinType' => QueryInterface::JOIN_TYPE_INNER, + 'fields' => false + ]) + ->getMatching(); + $this->_addAssociationsToTypeMap($this->getRepository(), $this->getTypeMap(), $result); + $this->_dirty(); + + return $this; + } + + /** + * Adds filtering conditions to this query to only bring rows that have no match + * to another from an associated table, based on conditions in the associated table. + * + * This function will add entries in the `contain` graph. + * + * ### Example: + * + * ``` + * // Bring only articles that were not tagged with 'cake' + * $query->notMatching('Tags', function ($q) { + * return $q->where(['name' => 'cake']); + * ); + * ``` + * + * It is possible to filter by deep associations by using dot notation: + * + * ### Example: + * + * ``` + * // Bring only articles that weren't commented by 'markstory' + * $query->notMatching('Comments.Users', function ($q) { + * return $q->where(['username' => 'markstory']); + * ); + * ``` + * + * As this function will create a `LEFT JOIN`, you might want to consider + * calling `distinct` on this query as you might get duplicate rows if + * your conditions don't filter them already. This might be the case, for example, + * of the same article having multiple comments. + * + * ### Example: + * + * ``` + * // Bring unique articles that were commented by 'markstory' + * $query->distinct(['Articles.id']) + * ->notMatching('Comments.Users', function ($q) { + * return $q->where(['username' => 'markstory']); + * ); + * ``` + * + * Please note that the query passed to the closure will only accept calling + * `select`, `where`, `andWhere` and `orWhere` on it. If you wish to + * add more complex clauses you can do it directly in the main query. + * + * @param string $assoc The association to filter by + * @param callable|null $builder a function that will receive a pre-made query object + * that can be used to add custom conditions or selecting some fields + * @return $this + */ + public function notMatching($assoc, callable $builder = null) + { + $result = $this->getEagerLoader() + ->setMatching($assoc, $builder, [ + 'joinType' => QueryInterface::JOIN_TYPE_LEFT, + 'fields' => false, + 'negateMatch' => true + ]) + ->getMatching(); + $this->_addAssociationsToTypeMap($this->getRepository(), $this->getTypeMap(), $result); + $this->_dirty(); + + return $this; + } + + /** + * {@inheritDoc} + * + * Populates or adds parts to current query clauses using an array. + * This is handy for passing all query clauses at once. The option array accepts: + * + * - fields: Maps to the select method + * - conditions: Maps to the where method + * - limit: Maps to the limit method + * - order: Maps to the order method + * - offset: Maps to the offset method + * - group: Maps to the group method + * - having: Maps to the having method + * - contain: Maps to the contain options for eager loading + * - join: Maps to the join method + * - page: Maps to the page method + * + * ### Example: + * + * ``` + * $query->applyOptions([ + * 'fields' => ['id', 'name'], + * 'conditions' => [ + * 'created >=' => '2013-01-01' + * ], + * 'limit' => 10 + * ]); + * ``` + * + * Is equivalent to: + * + * ``` + * $query + * ->select(['id', 'name']) + * ->where(['created >=' => '2013-01-01']) + * ->limit(10) + * ``` + */ + public function applyOptions(array $options) + { + $valid = [ + 'fields' => 'select', + 'conditions' => 'where', + 'join' => 'join', + 'order' => 'order', + 'limit' => 'limit', + 'offset' => 'offset', + 'group' => 'group', + 'having' => 'having', + 'contain' => 'contain', + 'page' => 'page', + ]; + + ksort($options); + foreach ($options as $option => $values) { + if (isset($valid[$option], $values)) { + $this->{$valid[$option]}($values); + } else { + $this->_options[$option] = $values; + } + } + + return $this; + } + + /** + * Creates a copy of this current query, triggers beforeFind and resets some state. + * + * The following state will be cleared: + * + * - autoFields + * - limit + * - offset + * - map/reduce functions + * - result formatters + * - order + * - containments + * + * This method creates query clones that are useful when working with subqueries. + * + * @return \Cake\ORM\Query + */ + public function cleanCopy() + { + $clone = clone $this; + $clone->setEagerLoader(clone $this->getEagerLoader()); + $clone->triggerBeforeFind(); + $clone->enableAutoFields(false); + $clone->limit(null); + $clone->order([], true); + $clone->offset(null); + $clone->mapReduce(null, null, true); + $clone->formatResults(null, true); + $clone->setSelectTypeMap(new TypeMap()); + $clone->decorateResults(null, true); + + return $clone; + } + + /** + * Object clone hook. + * + * Destroys the clones inner iterator and clones the value binder, and eagerloader instances. + * + * @return void + */ + public function __clone() + { + parent::__clone(); + if ($this->_eagerLoader) { + $this->_eagerLoader = clone $this->_eagerLoader; + } + } + + /** + * {@inheritDoc} + * + * Returns the COUNT(*) for the query. If the query has not been + * modified, and the count has already been performed the cached + * value is returned + */ + public function count() + { + if ($this->_resultsCount === null) { + $this->_resultsCount = $this->_performCount(); + } + + return $this->_resultsCount; + } + + /** + * Performs and returns the COUNT(*) for the query. + * + * @return int + */ + protected function _performCount() + { + $query = $this->cleanCopy(); + $counter = $this->_counter; + if ($counter) { + $query->counter(null); + + return (int)$counter($query); + } + + $complex = ( + $query->clause('distinct') || + count($query->clause('group')) || + count($query->clause('union')) || + $query->clause('having') + ); + + if (!$complex) { + // Expression fields could have bound parameters. + foreach ($query->clause('select') as $field) { + if ($field instanceof ExpressionInterface) { + $complex = true; + break; + } + } + } + + if (!$complex && $this->_valueBinder !== null) { + $order = $this->clause('order'); + $complex = $order === null ? false : $order->hasNestedExpression(); + } + + $count = ['count' => $query->func()->count('*')]; + + if (!$complex) { + $query->getEagerLoader()->enableAutoFields(false); + $statement = $query + ->select($count, true) + ->enableAutoFields(false) + ->execute(); + } else { + $statement = $this->getConnection()->newQuery() + ->select($count) + ->from(['count_source' => $query]) + ->execute(); + } + + $result = $statement->fetch('assoc')['count']; + $statement->closeCursor(); + + return (int)$result; + } + + /** + * Registers a callable function that will be executed when the `count` method in + * this query is called. The return value for the function will be set as the + * return value of the `count` method. + * + * This is particularly useful when you need to optimize a query for returning the + * count, for example removing unnecessary joins, removing group by or just return + * an estimated number of rows. + * + * The callback will receive as first argument a clone of this query and not this + * query itself. + * + * If the first param is a null value, the built-in counter function will be called + * instead + * + * @param callable|null $counter The counter value + * @return $this + */ + public function counter($counter) + { + $this->_counter = $counter; + + return $this; + } + + /** + * Toggle hydrating entities. + * + * If set to false array results will be returned for the query. + * + * @param bool $enable Use a boolean to set the hydration mode. + * @return $this + */ + public function enableHydration($enable = true) + { + $this->_dirty(); + $this->_hydrate = (bool)$enable; + + return $this; + } + + /** + * Returns the current hydration mode. + * + * @return bool + */ + public function isHydrationEnabled() + { + return $this->_hydrate; + } + + /** + * Toggle hydrating entities. + * + * If set to false array results will be returned. + * + * @deprecated 3.4.0 Use enableHydration()/isHydrationEnabled() instead. + * @param bool|null $enable Use a boolean to set the hydration mode. + * Null will fetch the current hydration mode. + * @return bool|$this A boolean when reading, and $this when setting the mode. + */ + public function hydrate($enable = null) + { + deprecationWarning( + 'Query::hydrate() is deprecated. ' . + 'Use enableHydration()/isHydrationEnabled() instead.' + ); + if ($enable === null) { + return $this->isHydrationEnabled(); + } + + return $this->enableHydration($enable); + } + + /** + * {@inheritDoc} + * + * @return $this + * @throws \RuntimeException When you attempt to cache a non-select query. + */ + public function cache($key, $config = 'default') + { + if ($this->_type !== 'select' && $this->_type !== null) { + throw new RuntimeException('You cannot cache the results of non-select queries.'); + } + + return $this->_cache($key, $config); + } + + /** + * {@inheritDoc} + * + * @throws \RuntimeException if this method is called on a non-select Query. + */ + public function all() + { + if ($this->_type !== 'select' && $this->_type !== null) { + throw new RuntimeException( + 'You cannot call all() on a non-select query. Use execute() instead.' + ); + } + + return $this->_all(); + } + + /** + * Trigger the beforeFind event on the query's repository object. + * + * Will not trigger more than once, and only for select queries. + * + * @return void + */ + public function triggerBeforeFind() + { + if (!$this->_beforeFindFired && $this->_type === 'select') { + $table = $this->getRepository(); + $this->_beforeFindFired = true; + /* @var \Cake\Event\EventDispatcherInterface $table */ + $table->dispatchEvent('Model.beforeFind', [ + $this, + new ArrayObject($this->_options), + !$this->isEagerLoaded() + ]); + } + } + + /** + * {@inheritDoc} + */ + public function sql(ValueBinder $binder = null) + { + $this->triggerBeforeFind(); + + $this->_transformQuery(); + + return parent::sql($binder); + } + + /** + * Executes this query and returns a ResultSet object containing the results. + * This will also setup the correct statement class in order to eager load deep + * associations. + * + * @return \Cake\ORM\ResultSet + */ + protected function _execute() + { + $this->triggerBeforeFind(); + if ($this->_results) { + $decorator = $this->_decoratorClass(); + + return new $decorator($this->_results); + } + + $statement = $this->getEagerLoader()->loadExternal($this, $this->execute()); + + return new ResultSet($this, $statement); + } + + /** + * Applies some defaults to the query object before it is executed. + * + * Specifically add the FROM clause, adds default table fields if none are + * specified and applies the joins required to eager load associations defined + * using `contain` + * + * It also sets the default types for the columns in the select clause + * + * @see \Cake\Database\Query::execute() + * @return void + */ + protected function _transformQuery() + { + if (!$this->_dirty || $this->_type !== 'select') { + return; + } + + if (empty($this->_parts['from'])) { + $this->from([$this->_repository->getAlias() => $this->_repository->getTable()]); + } + $this->_addDefaultFields(); + $this->getEagerLoader()->attachAssociations($this, $this->_repository, !$this->_hasFields); + $this->_addDefaultSelectTypes(); + } + + /** + * Inspects if there are any set fields for selecting, otherwise adds all + * the fields for the default table. + * + * @return void + */ + protected function _addDefaultFields() + { + $select = $this->clause('select'); + $this->_hasFields = true; + + if (!count($select) || $this->_autoFields === true) { + $this->_hasFields = false; + $this->select($this->getRepository()->getSchema()->columns()); + $select = $this->clause('select'); + } + + $aliased = $this->aliasFields($select, $this->getRepository()->getAlias()); + $this->select($aliased, true); + } + + /** + * Sets the default types for converting the fields in the select clause + * + * @return void + */ + protected function _addDefaultSelectTypes() + { + $typeMap = $this->getTypeMap()->getDefaults(); + $select = $this->clause('select'); + $types = []; + + foreach ($select as $alias => $value) { + if (isset($typeMap[$alias])) { + $types[$alias] = $typeMap[$alias]; + continue; + } + if (is_string($value) && isset($typeMap[$value])) { + $types[$alias] = $typeMap[$value]; + } + if ($value instanceof TypedResultInterface) { + $types[$alias] = $value->getReturnType(); + } + } + $this->getSelectTypeMap()->addDefaults($types); + } + + /** + * {@inheritDoc} + * + * @see \Cake\ORM\Table::find() + */ + public function find($finder, array $options = []) + { + return $this->getRepository()->callFinder($finder, $this, $options); + } + + /** + * Marks a query as dirty, removing any preprocessed information + * from in memory caching such as previous results + * + * @return void + */ + protected function _dirty() + { + $this->_results = null; + $this->_resultsCount = null; + parent::_dirty(); + } + + /** + * Create an update query. + * + * This changes the query type to be 'update'. + * Can be combined with set() and where() methods to create update queries. + * + * @param string|null $table Unused parameter. + * @return $this + */ + public function update($table = null) + { + $table = $table ?: $this->getRepository()->getTable(); + + return parent::update($table); + } + + /** + * Create a delete query. + * + * This changes the query type to be 'delete'. + * Can be combined with the where() method to create delete queries. + * + * @param string|null $table Unused parameter. + * @return $this + */ + public function delete($table = null) + { + $repo = $this->getRepository(); + $this->from([$repo->getAlias() => $repo->getTable()]); + + return parent::delete(); + } + + /** + * Create an insert query. + * + * This changes the query type to be 'insert'. + * Note calling this method will reset any data previously set + * with Query::values() + * + * Can be combined with the where() method to create delete queries. + * + * @param array $columns The columns to insert into. + * @param array $types A map between columns & their datatypes. + * @return $this + */ + public function insert(array $columns, array $types = []) + { + $table = $this->getRepository()->getTable(); + $this->into($table); + + return parent::insert($columns, $types); + } + + /** + * {@inheritDoc} + * + * @throws \BadMethodCallException if the method is called for a non-select query + */ + public function __call($method, $arguments) + { + if ($this->type() === 'select') { + return $this->_call($method, $arguments); + } + + throw new \BadMethodCallException( + sprintf('Cannot call method "%s" on a "%s" query', $method, $this->type()) + ); + } + + /** + * {@inheritDoc} + */ + public function __debugInfo() + { + $eagerLoader = $this->getEagerLoader(); + + return parent::__debugInfo() + [ + 'hydrate' => $this->_hydrate, + 'buffered' => $this->_useBufferedResults, + 'formatters' => count($this->_formatters), + 'mapReducers' => count($this->_mapReduce), + 'contain' => $eagerLoader ? $eagerLoader->getContain() : [], + 'matching' => $eagerLoader ? $eagerLoader->getMatching() : [], + 'extraOptions' => $this->_options, + 'repository' => $this->_repository + ]; + } + + /** + * Executes the query and converts the result set into JSON. + * + * Part of JsonSerializable interface. + * + * @return \Cake\Datasource\ResultSetInterface The data to convert to JSON. + */ + public function jsonSerialize() + { + return $this->all(); + } + + /** + * Sets whether or not the ORM should automatically append fields. + * + * By default calling select() will disable auto-fields. You can re-enable + * auto-fields with this method. + * + * @param bool $value Set true to enable, false to disable. + * @return $this + */ + public function enableAutoFields($value = true) + { + $this->_autoFields = (bool)$value; + + return $this; + } + + /** + * Gets whether or not the ORM should automatically append fields. + * + * By default calling select() will disable auto-fields. You can re-enable + * auto-fields with enableAutoFields(). + * + * @return bool The current value. + */ + public function isAutoFieldsEnabled() + { + return $this->_autoFields; + } + + /** + * Get/Set whether or not the ORM should automatically append fields. + * + * By default calling select() will disable auto-fields. You can re-enable + * auto-fields with this method. + * + * @deprecated 3.4.0 Use enableAutoFields()/isAutoFieldsEnabled() instead. + * @param bool|null $value The value to set or null to read the current value. + * @return bool|$this Either the current value or the query object. + */ + public function autoFields($value = null) + { + deprecationWarning( + 'Query::autoFields() is deprecated. ' . + 'Use enableAutoFields()/isAutoFieldsEnabled() instead.' + ); + if ($value === null) { + return $this->isAutoFieldsEnabled(); + } + + return $this->enableAutoFields($value); + } + + /** + * Decorates the results iterator with MapReduce routines and formatters + * + * @param \Traversable $result Original results + * @return \Cake\Datasource\ResultSetInterface + */ + protected function _decorateResults($result) + { + $result = $this->_applyDecorators($result); + + if (!($result instanceof ResultSet) && $this->isBufferedResultsEnabled()) { + $class = $this->_decoratorClass(); + $result = new $class($result->buffered()); + } + + return $result; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/README.md b/app/vendor/cakephp/cakephp/src/ORM/README.md new file mode 100644 index 000000000..9cbcbac40 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/README.md @@ -0,0 +1,169 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/orm.svg?style=flat-square)](https://packagist.org/packages/cakephp/orm) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# CakePHP ORM + +The CakePHP ORM provides a powerful and flexible way to work with relational +databases. Using a datamapper pattern the ORM allows you to manipulate data as +entities allowing you to create expressive domain layers in your applications. + +## Database engines supported + +The CakePHP ORM is compatible with: + +* MySQL 5.1+ +* Postgres 8+ +* SQLite3 +* SQLServer 2008+ +* Oracle (through a [community plugin](https://github.com/CakeDC/cakephp-oracle-driver)) + +## Connecting to the Database + +The first thing you need to do when using this library is register a connection +object. Before performing any operations with the connection, you need to +specify a driver to use: + +```php +use Cake\Datasource\ConnectionManager; + +ConnectionManager::setConfig('default', [ + 'className' => 'Cake\Database\Connection', + 'driver' => 'Cake\Database\Driver\Mysql', + 'database' => 'test', + 'username' => 'root', + 'password' => 'secret', + 'cacheMetadata' => true, + 'quoteIdentifiers' => false, +]); +``` + +Once a 'default' connection is registered, it will be used by all the Table +mappers if no explicit connection is defined. + +## Using Table Locator + +In order to access table instances you need to use a *Table Locator*. + +```php +use Cake\ORM\Locator\TableLocator; + +$locator = new TableLocator(); +$articles = $locator->get('Articles'); +``` + +You can also use a trait for easy access to the locator instance: + +```php +use Cake\ORM\Locator\LocatorAwareTrait; + +$articles = $this->getTableLocator()->get('Articles'); +``` + +By default classes using `LocatorAwareTrait` will share a global locator instance. +You can inject your own locator instance into the object: + +```php +use Cake\ORM\Locator\TableLocator; +use Cake\ORM\Locator\LocatorAwareTrait; + +$locator = new TableLocator(); +$this->setTableLocator($locator); + +$articles = $this->getTableLocator()->get('Articles'); +``` + +## Creating Associations + +In your table classes you can define the relations between your tables. CakePHP's ORM +supports 4 association types out of the box: + +* belongsTo - E.g. Many articles belong to a user. +* hasOne - E.g. A user has one profile +* hasMany - E.g. A user has many articles +* belongsToMany - E.g. An article belongsToMany tags. + +You define associations in your table's `initialize()` method. See the +[documentation](https://book.cakephp.org/3.0/en/orm/associations.html) for +complete examples. + +## Reading Data + +Once you've defined some table classes you can read existing data in your tables: + +```php +use Cake\ORM\Locator\LocatorAwareTrait; + +$articles = $this->getTableLocator()->get('Articles'); +foreach ($articles->find() as $article) { + echo $article->title; +} +``` + +You can use the [query builder](https://book.cakephp.org/3.0/en/orm/query-builder.html) to create +complex queries, and a [variety of methods](https://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html) +to access your data. + +## Saving Data + +Table objects provide ways to convert request data into entities, and then persist +those entities to the database: + +```php +use Cake\ORM\Locator\LocatorAwareTrait; + +$data = [ + 'title' => 'My first article', + 'body' => 'It is a great article', + 'user_id' => 1, + 'tags' => [ + '_ids' => [1, 2, 3] + ], + 'comments' => [ + ['comment' => 'Good job'], + ['comment' => 'Awesome work'], + ] +]; + +$articles = $this->getTableLocator()->get('Articles'); +$article = $articles->newEntity($data, [ + 'associated' => ['Tags', 'Comments'] +]); +$articles->save($article, [ + 'associated' => ['Tags', 'Comments'] +]) +``` + +The above shows how you can easily marshal and save an entity and its +associations in a simple & powerful way. Consult the [ORM documentation](https://book.cakephp.org/3.0/en/orm/saving-data.html) +for more in-depth examples. + +## Deleting Data + +Once you have a reference to an entity, you can use it to delete data: + +```php +$articles = $this->getTableLocator()->get('Articles'); +$article = $articles->get(2); +$articles->delete($article); +``` + +## Meta Data Cache + +It is recommended to enable meta data cache for production systems to avoid performance issues. +For e.g. file system strategy your bootstrap file could look like this: +```php +use Cake\Cache\Engine\FileEngine; + +$cacheConfig = [ + 'className' => FileEngine::class, + 'duration' => '+1 year', + 'serialize' => true, + 'prefix' => 'orm_', +], +Cache::setConfig('_cake_model_', $cacheConfig); +``` + +## Additional Documentation + +Consult [the CakePHP ORM documentation](https://book.cakephp.org/3.0/en/orm.html) +for more in-depth documentation. diff --git a/app/vendor/cakephp/cakephp/src/ORM/ResultSet.php b/app/vendor/cakephp/cakephp/src/ORM/ResultSet.php new file mode 100644 index 000000000..725d583c7 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/ResultSet.php @@ -0,0 +1,627 @@ +getRepository(); + $this->_statement = $statement; + $this->_driver = $query->getConnection()->getDriver(); + $this->_defaultTable = $query->getRepository(); + $this->_calculateAssociationMap($query); + $this->_hydrate = $query->isHydrationEnabled(); + $this->_entityClass = $repository->getEntityClass(); + $this->_useBuffering = $query->isBufferedResultsEnabled(); + $this->_defaultAlias = $this->_defaultTable->getAlias(); + $this->_calculateColumnMap($query); + $this->_autoFields = $query->isAutoFieldsEnabled(); + + if ($this->_useBuffering) { + $count = $this->count(); + $this->_results = new SplFixedArray($count); + } + } + + /** + * Returns the current record in the result iterator + * + * Part of Iterator interface. + * + * @return array|object + */ + public function current() + { + return $this->_current; + } + + /** + * Returns the key of the current record in the iterator + * + * Part of Iterator interface. + * + * @return int + */ + public function key() + { + return $this->_index; + } + + /** + * Advances the iterator pointer to the next record + * + * Part of Iterator interface. + * + * @return void + */ + public function next() + { + $this->_index++; + } + + /** + * Rewinds a ResultSet. + * + * Part of Iterator interface. + * + * @throws \Cake\Database\Exception + * @return void + */ + public function rewind() + { + if ($this->_index == 0) { + return; + } + + if (!$this->_useBuffering) { + $msg = 'You cannot rewind an un-buffered ResultSet. Use Query::bufferResults() to get a buffered ResultSet.'; + throw new Exception($msg); + } + + $this->_index = 0; + } + + /** + * Whether there are more results to be fetched from the iterator + * + * Part of Iterator interface. + * + * @return bool + */ + public function valid() + { + if ($this->_useBuffering) { + $valid = $this->_index < $this->_count; + if ($valid && $this->_results[$this->_index] !== null) { + $this->_current = $this->_results[$this->_index]; + + return true; + } + if (!$valid) { + return $valid; + } + } + + $this->_current = $this->_fetchResult(); + $valid = $this->_current !== false; + + if ($valid && $this->_useBuffering) { + $this->_results[$this->_index] = $this->_current; + } + if (!$valid && $this->_statement !== null) { + $this->_statement->closeCursor(); + } + + return $valid; + } + + /** + * Get the first record from a result set. + * + * This method will also close the underlying statement cursor. + * + * @return array|object + */ + public function first() + { + foreach ($this as $result) { + if ($this->_statement && !$this->_useBuffering) { + $this->_statement->closeCursor(); + } + + return $result; + } + } + + /** + * Serializes a resultset. + * + * Part of Serializable interface. + * + * @return string Serialized object + */ + public function serialize() + { + if (!$this->_useBuffering) { + $msg = 'You cannot serialize an un-buffered ResultSet. Use Query::bufferResults() to get a buffered ResultSet.'; + throw new Exception($msg); + } + + while ($this->valid()) { + $this->next(); + } + + if ($this->_results instanceof SplFixedArray) { + return serialize($this->_results->toArray()); + } + + return serialize($this->_results); + } + + /** + * Unserializes a resultset. + * + * Part of Serializable interface. + * + * @param string $serialized Serialized object + * @return void + */ + public function unserialize($serialized) + { + $results = (array)(unserialize($serialized) ?: []); + $this->_results = SplFixedArray::fromArray($results); + $this->_useBuffering = true; + $this->_count = $this->_results->count(); + } + + /** + * Gives the number of rows in the result set. + * + * Part of the Countable interface. + * + * @return int + */ + public function count() + { + if ($this->_count !== null) { + return $this->_count; + } + if ($this->_statement) { + return $this->_count = $this->_statement->rowCount(); + } + + if ($this->_results instanceof SplFixedArray) { + $this->_count = $this->_results->count(); + } else { + $this->_count = count($this->_results); + } + + return $this->_count; + } + + /** + * Calculates the list of associations that should get eager loaded + * when fetching each record + * + * @param \Cake\ORM\Query $query The query from where to derive the associations + * @return void + */ + protected function _calculateAssociationMap($query) + { + $map = $query->getEagerLoader()->associationsMap($this->_defaultTable); + $this->_matchingMap = (new Collection($map)) + ->match(['matching' => true]) + ->indexBy('alias') + ->toArray(); + + $this->_containMap = (new Collection(array_reverse($map))) + ->match(['matching' => false]) + ->indexBy('nestKey') + ->toArray(); + } + + /** + * Creates a map of row keys out of the query select clause that can be + * used to hydrate nested result sets more quickly. + * + * @param \Cake\ORM\Query $query The query from where to derive the column map + * @return void + */ + protected function _calculateColumnMap($query) + { + $map = []; + foreach ($query->clause('select') as $key => $field) { + $key = trim($key, '"`[]'); + + if (strpos($key, '__') <= 0) { + $map[$this->_defaultAlias][$key] = $key; + continue; + } + + $parts = explode('__', $key, 2); + $map[$parts[0]][$key] = $parts[1]; + } + + foreach ($this->_matchingMap as $alias => $assoc) { + if (!isset($map[$alias])) { + continue; + } + $this->_matchingMapColumns[$alias] = $map[$alias]; + unset($map[$alias]); + } + + $this->_map = $map; + } + + /** + * Creates a map of Type converter classes for each of the columns that should + * be fetched by this object. + * + * @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level + * @return void + */ + protected function _calculateTypeMap() + { + deprecationWarning('ResultSet::_calculateTypeMap() is deprecated, and will be removed in 4.0.0.'); + } + + /** + * Returns the Type classes for each of the passed fields belonging to the + * table. + * + * @param \Cake\ORM\Table $table The table from which to get the schema + * @param array $fields The fields whitelist to use for fields in the schema. + * @return array + */ + protected function _getTypes($table, $fields) + { + $types = []; + $schema = $table->getSchema(); + $map = array_keys(Type::map() + ['string' => 1, 'text' => 1, 'boolean' => 1]); + $typeMap = array_combine( + $map, + array_map(['Cake\Database\Type', 'build'], $map) + ); + + foreach (['string', 'text'] as $t) { + if (get_class($typeMap[$t]) === 'Cake\Database\Type') { + unset($typeMap[$t]); + } + } + + foreach (array_intersect($fields, $schema->columns()) as $col) { + $typeName = $schema->getColumnType($col); + if (isset($typeMap[$typeName])) { + $types[$col] = $typeMap[$typeName]; + } + } + + return $types; + } + + /** + * Helper function to fetch the next result from the statement or + * seeded results. + * + * @return mixed + */ + protected function _fetchResult() + { + if (!$this->_statement) { + return false; + } + + $row = $this->_statement->fetch('assoc'); + if ($row === false) { + return $row; + } + + return $this->_groupResult($row); + } + + /** + * Correctly nests results keys including those coming from associations + * + * @param array $row Array containing columns and values or false if there is no results + * @return array Results + */ + protected function _groupResult($row) + { + $defaultAlias = $this->_defaultAlias; + $results = $presentAliases = []; + $options = [ + 'useSetters' => false, + 'markClean' => true, + 'markNew' => false, + 'guard' => false + ]; + + foreach ($this->_matchingMapColumns as $alias => $keys) { + $matching = $this->_matchingMap[$alias]; + $results['_matchingData'][$alias] = array_combine( + $keys, + array_intersect_key($row, $keys) + ); + if ($this->_hydrate) { + /* @var \Cake\ORM\Table $table */ + $table = $matching['instance']; + $options['source'] = $table->getRegistryAlias(); + /* @var \Cake\Datasource\EntityInterface $entity */ + $entity = new $matching['entityClass']($results['_matchingData'][$alias], $options); + $results['_matchingData'][$alias] = $entity; + } + } + + foreach ($this->_map as $table => $keys) { + $results[$table] = array_combine($keys, array_intersect_key($row, $keys)); + $presentAliases[$table] = true; + } + + unset($presentAliases[$defaultAlias]); + + foreach ($this->_containMap as $assoc) { + $alias = $assoc['nestKey']; + + if ($assoc['canBeJoined'] && empty($this->_map[$alias])) { + continue; + } + + /* @var \Cake\ORM\Association $instance */ + $instance = $assoc['instance']; + + if (!$assoc['canBeJoined'] && !isset($row[$alias])) { + $results = $instance->defaultRowValue($results, $assoc['canBeJoined']); + continue; + } + + if (!$assoc['canBeJoined']) { + $results[$alias] = $row[$alias]; + } + + $target = $instance->getTarget(); + $options['source'] = $target->getRegistryAlias(); + unset($presentAliases[$alias]); + + if ($assoc['canBeJoined'] && $this->_autoFields !== false) { + $hasData = false; + foreach ($results[$alias] as $v) { + if ($v !== null && $v !== []) { + $hasData = true; + break; + } + } + + if (!$hasData) { + $results[$alias] = null; + } + } + + if ($this->_hydrate && $results[$alias] !== null && $assoc['canBeJoined']) { + $entity = new $assoc['entityClass']($results[$alias], $options); + $results[$alias] = $entity; + } + + $results = $instance->transformRow($results, $alias, $assoc['canBeJoined'], $assoc['targetProperty']); + } + + foreach ($presentAliases as $alias => $present) { + if (!isset($results[$alias])) { + continue; + } + $results[$defaultAlias][$alias] = $results[$alias]; + } + + if (isset($results['_matchingData'])) { + $results[$defaultAlias]['_matchingData'] = $results['_matchingData']; + } + + $options['source'] = $this->_defaultTable->getRegistryAlias(); + if (isset($results[$defaultAlias])) { + $results = $results[$defaultAlias]; + } + if ($this->_hydrate && !($results instanceof EntityInterface)) { + $results = new $this->_entityClass($results, $options); + } + + return $results; + } + + /** + * Casts all values from a row brought from a table to the correct + * PHP type. + * + * @param string $alias The table object alias + * @param array $values The values to cast + * @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level + * @return array + */ + protected function _castValues($alias, $values) + { + deprecationWarning('ResultSet::_castValues() is deprecated, and will be removed in 4.0.0.'); + + return $values; + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + return [ + 'items' => $this->toArray(), + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Rule/ExistsIn.php b/app/vendor/cakephp/cakephp/src/ORM/Rule/ExistsIn.php new file mode 100644 index 000000000..d583120ba --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Rule/ExistsIn.php @@ -0,0 +1,160 @@ + false]; + $this->_options = $options; + + $this->_fields = (array)$fields; + $this->_repository = $repository; + } + + /** + * Performs the existence check + * + * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields + * @param array $options Options passed to the check, + * where the `repository` key is required. + * @throws \RuntimeException When the rule refers to an undefined association. + * @return bool + */ + public function __invoke(EntityInterface $entity, array $options) + { + if (is_string($this->_repository)) { + if (!$options['repository']->hasAssociation($this->_repository)) { + throw new RuntimeException(sprintf( + "ExistsIn rule for '%s' is invalid. '%s' is not associated with '%s'.", + implode(', ', $this->_fields), + $this->_repository, + get_class($options['repository']) + )); + } + $repository = $options['repository']->getAssociation($this->_repository); + $this->_repository = $repository; + } + + $fields = $this->_fields; + $source = $target = $this->_repository; + $isAssociation = $target instanceof Association; + $bindingKey = $isAssociation ? (array)$target->getBindingKey() : (array)$target->getPrimaryKey(); + $realTarget = $isAssociation ? $target->getTarget() : $target; + + if (!empty($options['_sourceTable']) && $realTarget === $options['_sourceTable']) { + return true; + } + + if (!empty($options['repository'])) { + $source = $options['repository']; + } + if ($source instanceof Association) { + $source = $source->getSource(); + } + + if (!$entity->extract($this->_fields, true)) { + return true; + } + + if ($this->_fieldsAreNull($entity, $source)) { + return true; + } + + if ($this->_options['allowNullableNulls']) { + $schema = $source->getSchema(); + foreach ($fields as $i => $field) { + if ($schema->getColumn($field) && $schema->isNullable($field) && $entity->get($field) === null) { + unset($bindingKey[$i], $fields[$i]); + } + } + } + + $primary = array_map( + [$target, 'aliasField'], + $bindingKey + ); + $conditions = array_combine( + $primary, + $entity->extract($fields) + ); + + return $target->exists($conditions); + } + + /** + * Checks whether or not the given entity fields are nullable and null. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to check. + * @param \Cake\ORM\Table $source The table to use schema from. + * @return bool + */ + protected function _fieldsAreNull($entity, $source) + { + $nulls = 0; + $schema = $source->getSchema(); + foreach ($this->_fields as $field) { + if ($schema->getColumn($field) && $schema->isNullable($field) && $entity->get($field) === null) { + $nulls++; + } + } + + return $nulls === count($this->_fields); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Rule/IsUnique.php b/app/vendor/cakephp/cakephp/src/ORM/Rule/IsUnique.php new file mode 100644 index 000000000..2d024ed16 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Rule/IsUnique.php @@ -0,0 +1,109 @@ +_fields = $fields; + $this->_options = $options + ['allowMultipleNulls' => true]; + } + + /** + * Performs the uniqueness check + * + * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields + * where the `repository` key is required. + * @param array $options Options passed to the check, + * @return bool + */ + public function __invoke(EntityInterface $entity, array $options) + { + if (!$entity->extract($this->_fields, true)) { + return true; + } + $allowMultipleNulls = $this->_options['allowMultipleNulls']; + + $alias = $options['repository']->getAlias(); + $conditions = $this->_alias($alias, $entity->extract($this->_fields), $allowMultipleNulls); + if ($entity->isNew() === false) { + $keys = (array)$options['repository']->getPrimaryKey(); + $keys = $this->_alias($alias, $entity->extract($keys), $allowMultipleNulls); + if (array_filter($keys, 'strlen')) { + $conditions['NOT'] = $keys; + } + } + + return !$options['repository']->exists($conditions); + } + + /** + * Add a model alias to all the keys in a set of conditions. + * + * Null values will be omitted from the generated conditions, + * as SQL UNIQUE indexes treat `NULL != NULL` + * + * @param string $alias The alias to add. + * @param array $conditions The conditions to alias. + * @param bool $multipleNulls Whether or not to allow multiple nulls. + * @return array + */ + protected function _alias($alias, $conditions, $multipleNulls) + { + $aliased = []; + foreach ($conditions as $key => $value) { + if ($multipleNulls) { + $aliased["$alias.$key"] = $value; + } else { + $aliased["$alias.$key IS"] = $value; + } + } + + return $aliased; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Rule/ValidCount.php b/app/vendor/cakephp/cakephp/src/ORM/Rule/ValidCount.php new file mode 100644 index 000000000..33a980812 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Rule/ValidCount.php @@ -0,0 +1,60 @@ +_field = $field; + } + + /** + * Performs the count check + * + * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields. + * @param array $options Options passed to the check. + * @return bool True if successful, else false. + */ + public function __invoke(EntityInterface $entity, array $options) + { + $value = $entity->{$this->_field}; + if (!is_array($value) && !$value instanceof Countable) { + return false; + } + + return Validation::comparison(count($value), $options['operator'], $options['count']); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/RulesChecker.php b/app/vendor/cakephp/cakephp/src/ORM/RulesChecker.php new file mode 100644 index 000000000..feea8c898 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/RulesChecker.php @@ -0,0 +1,142 @@ +add($rules->isUnique(['email'], 'The email should be unique')); + * ``` + * + * @param array $fields The list of fields to check for uniqueness. + * @param string|array|null $message The error message to show in case the rule does not pass. Can + * also be an array of options. When an array, the 'message' key can be used to provide a message. + * @return callable + */ + public function isUnique(array $fields, $message = null) + { + $options = []; + if (is_array($message)) { + $options = $message + ['message' => null]; + $message = $options['message']; + unset($options['message']); + } + if (!$message) { + if ($this->_useI18n) { + $message = __d('cake', 'This value is already in use'); + } else { + $message = 'This value is already in use'; + } + } + + $errorField = current($fields); + + return $this->_addError(new IsUnique($fields, $options), '_isUnique', compact('errorField', 'message')); + } + + /** + * Returns a callable that can be used as a rule for checking that the values + * extracted from the entity to check exist as the primary key in another table. + * + * This is useful for enforcing foreign key integrity checks. + * + * ### Example: + * + * ``` + * $rules->add($rules->existsIn('author_id', 'Authors', 'Invalid Author')); + * + * $rules->add($rules->existsIn('site_id', new SitesTable(), 'Invalid Site')); + * ``` + * + * Available $options are error 'message' and 'allowNullableNulls' flag. + * 'message' sets a custom error message. + * Set 'allowNullableNulls' to true to accept composite foreign keys where one or more nullable columns are null. + * + * @param string|array $field The field or list of fields to check for existence by + * primary key lookup in the other table. + * @param object|string $table The table name where the fields existence will be checked. + * @param string|array|null $message The error message to show in case the rule does not pass. Can + * also be an array of options. When an array, the 'message' key can be used to provide a message. + * @return callable + */ + public function existsIn($field, $table, $message = null) + { + $options = []; + if (is_array($message)) { + $options = $message + ['message' => null]; + $message = $options['message']; + unset($options['message']); + } + + if (!$message) { + if ($this->_useI18n) { + $message = __d('cake', 'This value does not exist'); + } else { + $message = 'This value does not exist'; + } + } + + $errorField = is_string($field) ? $field : current($field); + + return $this->_addError(new ExistsIn($field, $table, $options), '_existsIn', compact('errorField', 'message')); + } + + /** + * Validates the count of associated records. + * + * @param string $field The field to check the count on. + * @param int $count The expected count. + * @param string $operator The operator for the count comparison. + * @param string|null $message The error message to show in case the rule does not pass. + * @return callable + */ + public function validCount($field, $count = 0, $operator = '>', $message = null) + { + if (!$message) { + if ($this->_useI18n) { + $message = __d('cake', 'The count does not match {0}{1}', [$operator, $count]); + } else { + $message = sprintf('The count does not match %s%d', $operator, $count); + } + } + + $errorField = $field; + + return $this->_addError( + new ValidCount($field), + '_validCount', + compact('count', 'operator', 'errorField', 'message') + ); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/SaveOptionsBuilder.php b/app/vendor/cakephp/cakephp/src/ORM/SaveOptionsBuilder.php new file mode 100644 index 000000000..f3ea0b95c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/SaveOptionsBuilder.php @@ -0,0 +1,221 @@ +_table = $table; + $this->parseArrayOptions($options); + + parent::__construct(); + } + + /** + * Takes an options array and populates the option object with the data. + * + * This can be used to turn an options array into the object. + * + * @throws \InvalidArgumentException If a given option key does not exist. + * @param array $array Options array. + * @return \Cake\ORM\SaveOptionsBuilder + */ + public function parseArrayOptions($array) + { + foreach ($array as $key => $value) { + $this->{$key}($value); + } + + return $this; + } + + /** + * Set associated options. + * + * @param string|array $associated String or array of associations. + * @return \Cake\ORM\SaveOptionsBuilder + */ + public function associated($associated) + { + $associated = $this->_normalizeAssociations($associated); + $this->_associated($this->_table, $associated); + $this->_options['associated'] = $associated; + + return $this; + } + + /** + * Checks that the associations exists recursively. + * + * @param \Cake\ORM\Table $table Table object. + * @param array $associations An associations array. + * @return void + */ + protected function _associated(Table $table, array $associations) + { + foreach ($associations as $key => $associated) { + if (is_int($key)) { + $this->_checkAssociation($table, $associated); + continue; + } + $this->_checkAssociation($table, $key); + if (isset($associated['associated'])) { + $this->_associated($table->getAssociation($key)->getTarget(), $associated['associated']); + continue; + } + } + } + + /** + * Checks if an association exists. + * + * @throws \RuntimeException If no such association exists for the given table. + * @param \Cake\ORM\Table $table Table object. + * @param string $association Association name. + * @return void + */ + protected function _checkAssociation(Table $table, $association) + { + if (!$table->associations()->has($association)) { + throw new RuntimeException(sprintf('Table `%s` is not associated with `%s`', get_class($table), $association)); + } + } + + /** + * Set the guard option. + * + * @param bool $guard Guard the properties or not. + * @return \Cake\ORM\SaveOptionsBuilder + */ + public function guard($guard) + { + $this->_options['guard'] = (bool)$guard; + + return $this; + } + + /** + * Set the validation rule set to use. + * + * @param string $validate Name of the validation rule set to use. + * @return \Cake\ORM\SaveOptionsBuilder + */ + public function validate($validate) + { + $this->_table->getValidator($validate); + $this->_options['validate'] = $validate; + + return $this; + } + + /** + * Set check existing option. + * + * @param bool $checkExisting Guard the properties or not. + * @return \Cake\ORM\SaveOptionsBuilder + */ + public function checkExisting($checkExisting) + { + $this->_options['checkExisting'] = (bool)$checkExisting; + + return $this; + } + + /** + * Option to check the rules. + * + * @param bool $checkRules Check the rules or not. + * @return \Cake\ORM\SaveOptionsBuilder + */ + public function checkRules($checkRules) + { + $this->_options['checkRules'] = (bool)$checkRules; + + return $this; + } + + /** + * Sets the atomic option. + * + * @param bool $atomic Atomic or not. + * @return \Cake\ORM\SaveOptionsBuilder + */ + public function atomic($atomic) + { + $this->_options['atomic'] = (bool)$atomic; + + return $this; + } + + /** + * @return array + */ + public function toArray() + { + return $this->_options; + } + + /** + * Setting custom options. + * + * @param string $option Option key. + * @param mixed $value Option value. + * @return \Cake\ORM\SaveOptionsBuilder + */ + public function set($option, $value) + { + if (method_exists($this, $option)) { + return $this->{$option}($value); + } + $this->_options[$option] = $value; + + return $this; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/Table.php b/app/vendor/cakephp/cakephp/src/ORM/Table.php new file mode 100644 index 000000000..8feb704a4 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/Table.php @@ -0,0 +1,2945 @@ +findByUsername('mark'); + * ``` + * + * You can also combine conditions on multiple fields using either `Or` or `And`: + * + * ``` + * $query = $users->findByUsernameOrEmail('mark', 'mark@example.org'); + * ``` + * + * ### Bulk updates/deletes + * + * You can use Table::updateAll() and Table::deleteAll() to do bulk updates/deletes. + * You should be aware that events will *not* be fired for bulk updates/deletes. + * + * ### Callbacks/events + * + * Table objects provide a few callbacks/events you can hook into to augment/replace + * find operations. Each event uses the standard event subsystem in CakePHP + * + * - `beforeFind(Event $event, Query $query, ArrayObject $options, boolean $primary)` + * Fired before each find operation. By stopping the event and supplying a + * return value you can bypass the find operation entirely. Any changes done + * to the $query instance will be retained for the rest of the find. The + * $primary parameter indicates whether or not this is the root query, + * or an associated query. + * + * - `buildValidator(Event $event, Validator $validator, string $name)` + * Allows listeners to modify validation rules for the provided named validator. + * + * - `buildRules(Event $event, RulesChecker $rules)` + * Allows listeners to modify the rules checker by adding more rules. + * + * - `beforeRules(Event $event, EntityInterface $entity, ArrayObject $options, string $operation)` + * Fired before an entity is validated using the rules checker. By stopping this event, + * you can return the final value of the rules checking operation. + * + * - `afterRules(Event $event, EntityInterface $entity, ArrayObject $options, bool $result, string $operation)` + * Fired after the rules have been checked on the entity. By stopping this event, + * you can return the final value of the rules checking operation. + * + * - `beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)` + * Fired before each entity is saved. Stopping this event will abort the save + * operation. When the event is stopped the result of the event will be returned. + * + * - `afterSave(Event $event, EntityInterface $entity, ArrayObject $options)` + * Fired after an entity is saved. + * + * - `afterSaveCommit(Event $event, EntityInterface $entity, ArrayObject $options)` + * Fired after the transaction in which the save operation is wrapped has been committed. + * It’s also triggered for non atomic saves where database operations are implicitly committed. + * The event is triggered only for the primary table on which save() is directly called. + * The event is not triggered if a transaction is started before calling save. + * + * - `beforeDelete(Event $event, EntityInterface $entity, ArrayObject $options)` + * Fired before an entity is deleted. By stopping this event you will abort + * the delete operation. + * + * - `afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)` + * Fired after an entity has been deleted. + * + * @see \Cake\Event\EventManager for reference on the events system. + */ +class Table implements RepositoryInterface, EventListenerInterface, EventDispatcherInterface, ValidatorAwareInterface +{ + + use EventDispatcherTrait; + use RulesAwareTrait; + use ValidatorAwareTrait; + + /** + * The alias this object is assigned to validators as. + * + * @var string + */ + const VALIDATOR_PROVIDER_NAME = 'table'; + + /** + * The name of the event dispatched when a validator has been built. + * + * @var string + */ + const BUILD_VALIDATOR_EVENT = 'Model.buildValidator'; + + /** + * The rules class name that is used. + * + * @var string + */ + const RULES_CLASS = 'Cake\ORM\RulesChecker'; + + /** + * Name of the table as it can be found in the database + * + * @var string + */ + protected $_table; + + /** + * Human name giving to this particular instance. Multiple objects representing + * the same database table can exist by using different aliases. + * + * @var string + */ + protected $_alias; + + /** + * Connection instance + * + * @var \Cake\Database\Connection + */ + protected $_connection; + + /** + * The schema object containing a description of this table fields + * + * @var \Cake\Database\Schema\TableSchema + */ + protected $_schema; + + /** + * The name of the field that represents the primary key in the table + * + * @var string|array + */ + protected $_primaryKey; + + /** + * The name of the field that represents a human readable representation of a row + * + * @var string + */ + protected $_displayField; + + /** + * The associations container for this Table. + * + * @var \Cake\ORM\AssociationCollection + */ + protected $_associations; + + /** + * BehaviorRegistry for this table + * + * @var \Cake\ORM\BehaviorRegistry + */ + protected $_behaviors; + + /** + * The name of the class that represent a single row for this table + * + * @var string + */ + protected $_entityClass; + + /** + * Registry key used to create this table object + * + * @var string + */ + protected $_registryAlias; + + /** + * Initializes a new instance + * + * The $config array understands the following keys: + * + * - table: Name of the database table to represent + * - alias: Alias to be assigned to this table (default to table name) + * - connection: The connection instance to use + * - entityClass: The fully namespaced class name of the entity class that will + * represent rows in this table. + * - schema: A \Cake\Database\Schema\TableSchema object or an array that can be + * passed to it. + * - eventManager: An instance of an event manager to use for internal events + * - behaviors: A BehaviorRegistry. Generally not used outside of tests. + * - associations: An AssociationCollection instance. + * - validator: A Validator instance which is assigned as the "default" + * validation set, or an associative array, where key is the name of the + * validation set and value the Validator instance. + * + * @param array $config List of options for this table + */ + public function __construct(array $config = []) + { + if (!empty($config['registryAlias'])) { + $this->setRegistryAlias($config['registryAlias']); + } + if (!empty($config['table'])) { + $this->setTable($config['table']); + } + if (!empty($config['alias'])) { + $this->setAlias($config['alias']); + } + if (!empty($config['connection'])) { + $this->setConnection($config['connection']); + } + if (!empty($config['schema'])) { + $this->setSchema($config['schema']); + } + if (!empty($config['entityClass'])) { + $this->setEntityClass($config['entityClass']); + } + $eventManager = $behaviors = $associations = null; + if (!empty($config['eventManager'])) { + $eventManager = $config['eventManager']; + } + if (!empty($config['behaviors'])) { + $behaviors = $config['behaviors']; + } + if (!empty($config['associations'])) { + $associations = $config['associations']; + } + if (!empty($config['validator'])) { + if (!is_array($config['validator'])) { + $this->setValidator(static::DEFAULT_VALIDATOR, $config['validator']); + } else { + foreach ($config['validator'] as $name => $validator) { + $this->setValidator($name, $validator); + } + } + } + $this->_eventManager = $eventManager ?: new EventManager(); + $this->_behaviors = $behaviors ?: new BehaviorRegistry(); + $this->_behaviors->setTable($this); + $this->_associations = $associations ?: new AssociationCollection(); + + $this->initialize($config); + $this->_eventManager->on($this); + $this->dispatchEvent('Model.initialize'); + } + + /** + * Get the default connection name. + * + * This method is used to get the fallback connection name if an + * instance is created through the TableLocator without a connection. + * + * @return string + * @see \Cake\ORM\Locator\TableLocator::get() + */ + public static function defaultConnectionName() + { + return 'default'; + } + + /** + * Initialize a table instance. Called after the constructor. + * + * You can use this method to define associations, attach behaviors + * define validation and do any other initialization logic you need. + * + * ``` + * public function initialize(array $config) + * { + * $this->belongsTo('Users'); + * $this->belongsToMany('Tagging.Tags'); + * $this->setPrimaryKey('something_else'); + * } + * ``` + * + * @param array $config Configuration options passed to the constructor + * @return void + */ + public function initialize(array $config) + { + } + + /** + * Sets the database table name. + * + * @param string $table Table name. + * @return $this + */ + public function setTable($table) + { + $this->_table = $table; + + return $this; + } + + /** + * Returns the database table name. + * + * @return string + */ + public function getTable() + { + if ($this->_table === null) { + $table = namespaceSplit(get_class($this)); + $table = substr(end($table), 0, -5); + if (!$table) { + $table = $this->getAlias(); + } + $this->_table = Inflector::underscore($table); + } + + return $this->_table; + } + + /** + * Returns the database table name or sets a new one. + * + * @deprecated 3.4.0 Use setTable()/getTable() instead. + * @param string|null $table the new table name + * @return string + */ + public function table($table = null) + { + deprecationWarning( + get_called_class() . '::table() is deprecated. ' . + 'Use setTable()/getTable() instead.' + ); + if ($table !== null) { + $this->setTable($table); + } + + return $this->getTable(); + } + + /** + * Sets the table alias. + * + * @param string $alias Table alias + * @return $this + */ + public function setAlias($alias) + { + $this->_alias = $alias; + + return $this; + } + + /** + * Returns the table alias. + * + * @return string + */ + public function getAlias() + { + if ($this->_alias === null) { + $alias = namespaceSplit(get_class($this)); + $alias = substr(end($alias), 0, -5) ?: $this->_table; + $this->_alias = $alias; + } + + return $this->_alias; + } + + /** + * {@inheritDoc} + * @deprecated 3.4.0 Use setAlias()/getAlias() instead. + */ + public function alias($alias = null) + { + deprecationWarning( + get_called_class() . '::alias() is deprecated. ' . + 'Use setAlias()/getAlias() instead.' + ); + if ($alias !== null) { + $this->setAlias($alias); + } + + return $this->getAlias(); + } + + /** + * Alias a field with the table's current alias. + * + * If field is already aliased it will result in no-op. + * + * @param string $field The field to alias. + * @return string The field prefixed with the table alias. + */ + public function aliasField($field) + { + if (strpos($field, '.') !== false) { + return $field; + } + + return $this->getAlias() . '.' . $field; + } + + /** + * Sets the table registry key used to create this table instance. + * + * @param string $registryAlias The key used to access this object. + * @return $this + */ + public function setRegistryAlias($registryAlias) + { + $this->_registryAlias = $registryAlias; + + return $this; + } + + /** + * Returns the table registry key used to create this table instance. + * + * @return string + */ + public function getRegistryAlias() + { + if ($this->_registryAlias === null) { + $this->_registryAlias = $this->getAlias(); + } + + return $this->_registryAlias; + } + + /** + * Returns the table registry key used to create this table instance or sets one. + * + * @deprecated 3.4.0 Use setRegistryAlias()/getRegistryAlias() instead. + * @param string|null $registryAlias the key used to access this object + * @return string + */ + public function registryAlias($registryAlias = null) + { + deprecationWarning( + get_called_class() . '::registryAlias() is deprecated. ' . + 'Use setRegistryAlias()/getRegistryAlias() instead.' + ); + if ($registryAlias !== null) { + $this->setRegistryAlias($registryAlias); + } + + return $this->getRegistryAlias(); + } + + /** + * Sets the connection instance. + * + * @param \Cake\Database\Connection|\Cake\Datasource\ConnectionInterface $connection The connection instance + * @return $this + */ + public function setConnection(ConnectionInterface $connection) + { + $this->_connection = $connection; + + return $this; + } + + /** + * Returns the connection instance. + * + * @return \Cake\Database\Connection + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * Returns the connection instance or sets a new one + * + * @deprecated 3.4.0 Use setConnection()/getConnection() instead. + * @param \Cake\Datasource\ConnectionInterface|null $connection The new connection instance + * @return \Cake\Datasource\ConnectionInterface + */ + public function connection(ConnectionInterface $connection = null) + { + deprecationWarning( + get_called_class() . '::connection() is deprecated. ' . + 'Use setConnection()/getConnection() instead.' + ); + if ($connection !== null) { + $this->setConnection($connection); + } + + return $this->getConnection(); + } + + /** + * Returns the schema table object describing this table's properties. + * + * @return \Cake\Database\Schema\TableSchema + */ + public function getSchema() + { + if ($this->_schema === null) { + $this->_schema = $this->_initializeSchema( + $this->getConnection() + ->getSchemaCollection() + ->describe($this->getTable()) + ); + } + + return $this->_schema; + } + + /** + * Sets the schema table object describing this table's properties. + * + * If an array is passed, a new TableSchema will be constructed + * out of it and used as the schema for this table. + * + * @param array|\Cake\Database\Schema\TableSchema $schema Schema to be used for this table + * @return $this + */ + public function setSchema($schema) + { + if (is_array($schema)) { + $constraints = []; + + if (isset($schema['_constraints'])) { + $constraints = $schema['_constraints']; + unset($schema['_constraints']); + } + + $schema = new TableSchema($this->getTable(), $schema); + + foreach ($constraints as $name => $value) { + $schema->addConstraint($name, $value); + } + } + + $this->_schema = $schema; + + return $this; + } + + /** + * Returns the schema table object describing this table's properties. + * + * If a TableSchema is passed, it will be used for this table + * instead of the default one. + * + * If an array is passed, a new TableSchema will be constructed + * out of it and used as the schema for this table. + * + * @deprecated 3.4.0 Use setSchema()/getSchema() instead. + * @param array|\Cake\Database\Schema\TableSchema|null $schema New schema to be used for this table + * @return \Cake\Database\Schema\TableSchema + */ + public function schema($schema = null) + { + deprecationWarning( + get_called_class() . '::schema() is deprecated. ' . + 'Use setSchema()/getSchema() instead.' + ); + if ($schema !== null) { + $this->setSchema($schema); + } + + return $this->getSchema(); + } + + /** + * Override this function in order to alter the schema used by this table. + * This function is only called after fetching the schema out of the database. + * If you wish to provide your own schema to this table without touching the + * database, you can override schema() or inject the definitions though that + * method. + * + * ### Example: + * + * ``` + * protected function _initializeSchema(\Cake\Database\Schema\TableSchema $schema) { + * $schema->setColumnType('preferences', 'json'); + * return $schema; + * } + * ``` + * + * @param \Cake\Database\Schema\TableSchema $schema The table definition fetched from database. + * @return \Cake\Database\Schema\TableSchema the altered schema + */ + protected function _initializeSchema(TableSchema $schema) + { + return $schema; + } + + /** + * Test to see if a Table has a specific field/column. + * + * Delegates to the schema object and checks for column presence + * using the Schema\Table instance. + * + * @param string $field The field to check for. + * @return bool True if the field exists, false if it does not. + */ + public function hasField($field) + { + $schema = $this->getSchema(); + + return $schema->getColumn($field) !== null; + } + + /** + * Sets the primary key field name. + * + * @param string|array $key Sets a new name to be used as primary key + * @return $this + */ + public function setPrimaryKey($key) + { + $this->_primaryKey = $key; + + return $this; + } + + /** + * Returns the primary key field name. + * + * @return string|array + */ + public function getPrimaryKey() + { + if ($this->_primaryKey === null) { + $key = (array)$this->getSchema()->primaryKey(); + if (count($key) === 1) { + $key = $key[0]; + } + $this->_primaryKey = $key; + } + + return $this->_primaryKey; + } + + /** + * Returns the primary key field name or sets a new one + * + * @deprecated 3.4.0 Use setPrimaryKey()/getPrimaryKey() instead. + * @param string|array|null $key Sets a new name to be used as primary key + * @return string|array + */ + public function primaryKey($key = null) + { + deprecationWarning( + get_called_class() . '::primaryKey() is deprecated. ' . + 'Use setPrimaryKey()/getPrimaryKey() instead.' + ); + if ($key !== null) { + $this->setPrimaryKey($key); + } + + return $this->getPrimaryKey(); + } + + /** + * Sets the display field. + * + * @param string $key Name to be used as display field. + * @return $this + */ + public function setDisplayField($key) + { + $this->_displayField = $key; + + return $this; + } + + /** + * Returns the display field. + * + * @return string + */ + public function getDisplayField() + { + if ($this->_displayField === null) { + $schema = $this->getSchema(); + $primary = (array)$this->getPrimaryKey(); + $this->_displayField = array_shift($primary); + if ($schema->getColumn('title')) { + $this->_displayField = 'title'; + } + if ($schema->getColumn('name')) { + $this->_displayField = 'name'; + } + } + + return $this->_displayField; + } + + /** + * Returns the display field or sets a new one + * + * @deprecated 3.4.0 Use setDisplayField()/getDisplayField() instead. + * @param string|null $key sets a new name to be used as display field + * @return string + */ + public function displayField($key = null) + { + deprecationWarning( + get_called_class() . '::displayField() is deprecated. ' . + 'Use setDisplayField()/getDisplayField() instead.' + ); + if ($key !== null) { + $this->setDisplayField($key); + + return $key; + } + + return $this->getDisplayField(); + } + + /** + * Returns the class used to hydrate rows for this table. + * + * @return string + */ + public function getEntityClass() + { + if (!$this->_entityClass) { + $default = Entity::class; + $self = get_called_class(); + $parts = explode('\\', $self); + + if ($self === __CLASS__ || count($parts) < 3) { + return $this->_entityClass = $default; + } + + $alias = Inflector::singularize(substr(array_pop($parts), 0, -5)); + $name = implode('\\', array_slice($parts, 0, -1)) . '\\Entity\\' . $alias; + if (!class_exists($name)) { + return $this->_entityClass = $default; + } + + $class = App::className($name, 'Model/Entity'); + if (!$class) { + throw new MissingEntityException([$name]); + } + + $this->_entityClass = $class; + } + + return $this->_entityClass; + } + + /** + * Sets the class used to hydrate rows for this table. + * + * @param string $name The name of the class to use + * @throws \Cake\ORM\Exception\MissingEntityException when the entity class cannot be found + * @return $this + */ + public function setEntityClass($name) + { + $class = App::className($name, 'Model/Entity'); + if (!$class) { + throw new MissingEntityException([$name]); + } + + $this->_entityClass = $class; + + return $this; + } + + /** + * Returns the class used to hydrate rows for this table or sets + * a new one + * + * @deprecated 3.4.0 Use setEntityClass()/getEntityClass() instead. + * @param string|null $name The name of the class to use + * @throws \Cake\ORM\Exception\MissingEntityException when the entity class cannot be found + * @return string + */ + public function entityClass($name = null) + { + deprecationWarning( + get_called_class() . '::entityClass() is deprecated. ' . + 'Use setEntityClass()/getEntityClass() instead.' + ); + if ($name !== null) { + $this->setEntityClass($name); + } + + return $this->getEntityClass(); + } + + /** + * Add a behavior. + * + * Adds a behavior to this table's behavior collection. Behaviors + * provide an easy way to create horizontally re-usable features + * that can provide trait like functionality, and allow for events + * to be listened to. + * + * Example: + * + * Load a behavior, with some settings. + * + * ``` + * $this->addBehavior('Tree', ['parent' => 'parentId']); + * ``` + * + * Behaviors are generally loaded during Table::initialize(). + * + * @param string $name The name of the behavior. Can be a short class reference. + * @param array $options The options for the behavior to use. + * @return $this + * @throws \RuntimeException If a behavior is being reloaded. + * @see \Cake\ORM\Behavior + */ + public function addBehavior($name, array $options = []) + { + $this->_behaviors->load($name, $options); + + return $this; + } + + /** + * Adds an array of behaviors to the table's behavior collection. + * + * Example: + * + * ``` + * $this->addBehaviors([ + * 'Timestamp', + * 'Tree' => ['level' => 'level'], + * ]); + * ``` + * + * @param array $behaviors All of the behaviors to load. + * @return $this + * @throws \RuntimeException If a behavior is being reloaded. + */ + public function addBehaviors(array $behaviors) + { + foreach ($behaviors as $name => $options) { + if (is_int($name)) { + $name = $options; + $options = []; + } + + $this->addBehavior($name, $options); + } + + return $this; + } + + /** + * Removes a behavior from this table's behavior registry. + * + * Example: + * + * Remove a behavior from this table. + * + * ``` + * $this->removeBehavior('Tree'); + * ``` + * + * @param string $name The alias that the behavior was added with. + * @return $this + * @see \Cake\ORM\Behavior + */ + public function removeBehavior($name) + { + $this->_behaviors->unload($name); + + return $this; + } + + /** + * Returns the behavior registry for this table. + * + * @return \Cake\ORM\BehaviorRegistry The BehaviorRegistry instance. + */ + public function behaviors() + { + return $this->_behaviors; + } + + /** + * Get a behavior from the registry. + * + * @param string $name The behavior alias to get from the registry. + * @return \Cake\ORM\Behavior + * @throws \InvalidArgumentException If the behavior does not exist. + */ + public function getBehavior($name) + { + /** @var \Cake\ORM\Behavior $behavior */ + $behavior = $this->_behaviors->get($name); + if ($behavior === null) { + throw new InvalidArgumentException(sprintf( + 'The %s behavior is not defined on %s.', + $name, + get_class($this) + )); + } + + return $behavior; + } + + /** + * Check if a behavior with the given alias has been loaded. + * + * @param string $name The behavior alias to check. + * @return bool Whether or not the behavior exists. + */ + public function hasBehavior($name) + { + return $this->_behaviors->has($name); + } + + /** + * Returns an association object configured for the specified alias if any. + * + * @deprecated 3.6.0 Use getAssociation() and Table::hasAssociation() instead. + * @param string $name the alias used for the association. + * @return \Cake\ORM\Association|null Either the association or null. + */ + public function association($name) + { + deprecationWarning('Use Table::getAssociation() and Table::hasAssociation() instead.'); + + return $this->findAssociation($name); + } + + /** + * Returns an association object configured for the specified alias. + * + * The name argument also supports dot syntax to access deeper associations. + * + * ``` + * $users = $this->getAssociation('Articles.Comments.Users'); + * ``` + * + * Note that this method requires the association to be present or otherwise + * throws an exception. + * If you are not sure, use hasAssociation() before calling this method. + * + * @param string $name The alias used for the association. + * @return \Cake\ORM\Association The association. + * @throws \InvalidArgumentException + */ + public function getAssociation($name) + { + $association = $this->findAssociation($name); + if (!$association) { + throw new InvalidArgumentException("The {$name} association is not defined on {$this->getAlias()}."); + } + + return $association; + } + + /** + * Checks whether a specific association exists on this Table instance. + * + * The name argument also supports dot syntax to access deeper associations. + * + * ``` + * $hasUsers = $this->hasAssociation('Articles.Comments.Users'); + * ``` + * + * @param string $name The alias used for the association. + * @return bool + */ + public function hasAssociation($name) + { + return $this->findAssociation($name) !== null; + } + + /** + * Returns an association object configured for the specified alias if any. + * + * The name argument also supports dot syntax to access deeper associations. + * + * ``` + * $users = $this->getAssociation('Articles.Comments.Users'); + * ``` + * + * @param string $name The alias used for the association. + * @return \Cake\ORM\Association|null Either the association or null. + */ + protected function findAssociation($name) + { + if (strpos($name, '.') === false) { + return $this->_associations->get($name); + } + + list($name, $next) = array_pad(explode('.', $name, 2), 2, null); + $result = $this->_associations->get($name); + + if ($result !== null && $next !== null) { + $result = $result->getTarget()->getAssociation($next); + } + + return $result; + } + + /** + * Get the associations collection for this table. + * + * @return \Cake\ORM\AssociationCollection|\Cake\ORM\Association[] The collection of association objects. + */ + public function associations() + { + return $this->_associations; + } + + /** + * Setup multiple associations. + * + * It takes an array containing set of table names indexed by association type + * as argument: + * + * ``` + * $this->Posts->addAssociations([ + * 'belongsTo' => [ + * 'Users' => ['className' => 'App\Model\Table\UsersTable'] + * ], + * 'hasMany' => ['Comments'], + * 'belongsToMany' => ['Tags'] + * ]); + * ``` + * + * Each association type accepts multiple associations where the keys + * are the aliases, and the values are association config data. If numeric + * keys are used the values will be treated as association aliases. + * + * @param array $params Set of associations to bind (indexed by association type) + * @return $this + * @see \Cake\ORM\Table::belongsTo() + * @see \Cake\ORM\Table::hasOne() + * @see \Cake\ORM\Table::hasMany() + * @see \Cake\ORM\Table::belongsToMany() + */ + public function addAssociations(array $params) + { + foreach ($params as $assocType => $tables) { + foreach ($tables as $associated => $options) { + if (is_numeric($associated)) { + $associated = $options; + $options = []; + } + $this->{$assocType}($associated, $options); + } + } + + return $this; + } + + /** + * Creates a new BelongsTo association between this table and a target + * table. A "belongs to" association is a N-1 relationship where this table + * is the N side, and where there is a single associated record in the target + * table for each one in this table. + * + * Target table can be inferred by its name, which is provided in the + * first argument, or you can either pass the to be instantiated or + * an instance of it directly. + * + * The options array accept the following keys: + * + * - className: The class name of the target table object + * - targetTable: An instance of a table object to be used as the target table + * - foreignKey: The name of the field to use as foreign key, if false none + * will be used + * - conditions: array with a list of conditions to filter the join with + * - joinType: The type of join to be used (e.g. INNER) + * - strategy: The loading strategy to use. 'join' and 'select' are supported. + * - finder: The finder method to use when loading records from this association. + * Defaults to 'all'. When the strategy is 'join', only the fields, containments, + * and where conditions will be used from the finder. + * + * This method will return the association object that was built. + * + * @param string $associated the alias for the target table. This is used to + * uniquely identify the association + * @param array $options list of options to configure the association definition + * @return \Cake\ORM\Association\BelongsTo + */ + public function belongsTo($associated, array $options = []) + { + $options += ['sourceTable' => $this]; + + /** @var \Cake\ORM\Association\BelongsTo $association */ + $association = $this->_associations->load(BelongsTo::class, $associated, $options); + + return $association; + } + + /** + * Creates a new HasOne association between this table and a target + * table. A "has one" association is a 1-1 relationship. + * + * Target table can be inferred by its name, which is provided in the + * first argument, or you can either pass the class name to be instantiated or + * an instance of it directly. + * + * The options array accept the following keys: + * + * - className: The class name of the target table object + * - targetTable: An instance of a table object to be used as the target table + * - foreignKey: The name of the field to use as foreign key, if false none + * will be used + * - dependent: Set to true if you want CakePHP to cascade deletes to the + * associated table when an entity is removed on this table. The delete operation + * on the associated table will not cascade further. To get recursive cascades enable + * `cascadeCallbacks` as well. Set to false if you don't want CakePHP to remove + * associated data, or when you are using database constraints. + * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on + * cascaded deletes. If false the ORM will use deleteAll() to remove data. + * When true records will be loaded and then deleted. + * - conditions: array with a list of conditions to filter the join with + * - joinType: The type of join to be used (e.g. LEFT) + * - strategy: The loading strategy to use. 'join' and 'select' are supported. + * - finder: The finder method to use when loading records from this association. + * Defaults to 'all'. When the strategy is 'join', only the fields, containments, + * and where conditions will be used from the finder. + * + * This method will return the association object that was built. + * + * @param string $associated the alias for the target table. This is used to + * uniquely identify the association + * @param array $options list of options to configure the association definition + * @return \Cake\ORM\Association\HasOne + */ + public function hasOne($associated, array $options = []) + { + $options += ['sourceTable' => $this]; + + /** @var \Cake\ORM\Association\HasOne $association */ + $association = $this->_associations->load(HasOne::class, $associated, $options); + + return $association; + } + + /** + * Creates a new HasMany association between this table and a target + * table. A "has many" association is a 1-N relationship. + * + * Target table can be inferred by its name, which is provided in the + * first argument, or you can either pass the class name to be instantiated or + * an instance of it directly. + * + * The options array accept the following keys: + * + * - className: The class name of the target table object + * - targetTable: An instance of a table object to be used as the target table + * - foreignKey: The name of the field to use as foreign key, if false none + * will be used + * - dependent: Set to true if you want CakePHP to cascade deletes to the + * associated table when an entity is removed on this table. The delete operation + * on the associated table will not cascade further. To get recursive cascades enable + * `cascadeCallbacks` as well. Set to false if you don't want CakePHP to remove + * associated data, or when you are using database constraints. + * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on + * cascaded deletes. If false the ORM will use deleteAll() to remove data. + * When true records will be loaded and then deleted. + * - conditions: array with a list of conditions to filter the join with + * - sort: The order in which results for this association should be returned + * - saveStrategy: Either 'append' or 'replace'. When 'append' the current records + * are appended to any records in the database. When 'replace' associated records + * not in the current set will be removed. If the foreign key is a null able column + * or if `dependent` is true records will be orphaned. + * - strategy: The strategy to be used for selecting results Either 'select' + * or 'subquery'. If subquery is selected the query used to return results + * in the source table will be used as conditions for getting rows in the + * target table. + * - finder: The finder method to use when loading records from this association. + * Defaults to 'all'. + * + * This method will return the association object that was built. + * + * @param string $associated the alias for the target table. This is used to + * uniquely identify the association + * @param array $options list of options to configure the association definition + * @return \Cake\ORM\Association\HasMany + */ + public function hasMany($associated, array $options = []) + { + $options += ['sourceTable' => $this]; + + /** @var \Cake\ORM\Association\HasMany $association */ + $association = $this->_associations->load(HasMany::class, $associated, $options); + + return $association; + } + + /** + * Creates a new BelongsToMany association between this table and a target + * table. A "belongs to many" association is a M-N relationship. + * + * Target table can be inferred by its name, which is provided in the + * first argument, or you can either pass the class name to be instantiated or + * an instance of it directly. + * + * The options array accept the following keys: + * + * - className: The class name of the target table object. + * - targetTable: An instance of a table object to be used as the target table. + * - foreignKey: The name of the field to use as foreign key. + * - targetForeignKey: The name of the field to use as the target foreign key. + * - joinTable: The name of the table representing the link between the two + * - through: If you choose to use an already instantiated link table, set this + * key to a configured Table instance containing associations to both the source + * and target tables in this association. + * - dependent: Set to false, if you do not want junction table records removed + * when an owning record is removed. + * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on + * cascaded deletes. If false the ORM will use deleteAll() to remove data. + * When true join/junction table records will be loaded and then deleted. + * - conditions: array with a list of conditions to filter the join with. + * - sort: The order in which results for this association should be returned. + * - strategy: The strategy to be used for selecting results Either 'select' + * or 'subquery'. If subquery is selected the query used to return results + * in the source table will be used as conditions for getting rows in the + * target table. + * - saveStrategy: Either 'append' or 'replace'. Indicates the mode to be used + * for saving associated entities. The former will only create new links + * between both side of the relation and the latter will do a wipe and + * replace to create the links between the passed entities when saving. + * - strategy: The loading strategy to use. 'select' and 'subquery' are supported. + * - finder: The finder method to use when loading records from this association. + * Defaults to 'all'. + * + * This method will return the association object that was built. + * + * @param string $associated the alias for the target table. This is used to + * uniquely identify the association + * @param array $options list of options to configure the association definition + * @return \Cake\ORM\Association\BelongsToMany + */ + public function belongsToMany($associated, array $options = []) + { + $options += ['sourceTable' => $this]; + + /** @var \Cake\ORM\Association\BelongsToMany $association */ + $association = $this->_associations->load(BelongsToMany::class, $associated, $options); + + return $association; + } + + /** + * Creates a new Query for this repository and applies some defaults based on the + * type of search that was selected. + * + * ### Model.beforeFind event + * + * Each find() will trigger a `Model.beforeFind` event for all attached + * listeners. Any listener can set a valid result set using $query + * + * By default, `$options` will recognize the following keys: + * + * - fields + * - conditions + * - order + * - limit + * - offset + * - page + * - group + * - having + * - contain + * - join + * + * ### Usage + * + * Using the options array: + * + * ``` + * $query = $articles->find('all', [ + * 'conditions' => ['published' => 1], + * 'limit' => 10, + * 'contain' => ['Users', 'Comments'] + * ]); + * ``` + * + * Using the builder interface: + * + * ``` + * $query = $articles->find() + * ->where(['published' => 1]) + * ->limit(10) + * ->contain(['Users', 'Comments']); + * ``` + * + * ### Calling finders + * + * The find() method is the entry point for custom finder methods. + * You can invoke a finder by specifying the type: + * + * ``` + * $query = $articles->find('published'); + * ``` + * + * Would invoke the `findPublished` method. + * + * @param string $type the type of query to perform + * @param array|\ArrayAccess $options An array that will be passed to Query::applyOptions() + * @return \Cake\ORM\Query The query builder + */ + public function find($type = 'all', $options = []) + { + $query = $this->query(); + $query->select(); + + return $this->callFinder($type, $query, $options); + } + + /** + * Returns the query as passed. + * + * By default findAll() applies no conditions, you + * can override this method in subclasses to modify how `find('all')` works. + * + * @param \Cake\ORM\Query $query The query to find with + * @param array $options The options to use for the find + * @return \Cake\ORM\Query The query builder + */ + public function findAll(Query $query, array $options) + { + return $query; + } + + /** + * Sets up a query object so results appear as an indexed array, useful for any + * place where you would want a list such as for populating input select boxes. + * + * When calling this finder, the fields passed are used to determine what should + * be used as the array key, value and optionally what to group the results by. + * By default the primary key for the model is used for the key, and the display + * field as value. + * + * The results of this finder will be in the following form: + * + * ``` + * [ + * 1 => 'value for id 1', + * 2 => 'value for id 2', + * 4 => 'value for id 4' + * ] + * ``` + * + * You can specify which property will be used as the key and which as value + * by using the `$options` array, when not specified, it will use the results + * of calling `primaryKey` and `displayField` respectively in this table: + * + * ``` + * $table->find('list', [ + * 'keyField' => 'name', + * 'valueField' => 'age' + * ]); + * ``` + * + * Results can be put together in bigger groups when they share a property, you + * can customize the property to use for grouping by setting `groupField`: + * + * ``` + * $table->find('list', [ + * 'groupField' => 'category_id', + * ]); + * ``` + * + * When using a `groupField` results will be returned in this format: + * + * ``` + * [ + * 'group_1' => [ + * 1 => 'value for id 1', + * 2 => 'value for id 2', + * ] + * 'group_2' => [ + * 4 => 'value for id 4' + * ] + * ] + * ``` + * + * @param \Cake\ORM\Query $query The query to find with + * @param array $options The options for the find + * @return \Cake\ORM\Query The query builder + */ + public function findList(Query $query, array $options) + { + $options += [ + 'keyField' => $this->getPrimaryKey(), + 'valueField' => $this->getDisplayField(), + 'groupField' => null + ]; + + if (isset($options['idField'])) { + $options['keyField'] = $options['idField']; + unset($options['idField']); + deprecationWarning('Option "idField" is deprecated, use "keyField" instead.'); + } + + if (!$query->clause('select') && + !is_object($options['keyField']) && + !is_object($options['valueField']) && + !is_object($options['groupField']) + ) { + $fields = array_merge( + (array)$options['keyField'], + (array)$options['valueField'], + (array)$options['groupField'] + ); + $columns = $this->getSchema()->columns(); + if (count($fields) === count(array_intersect($fields, $columns))) { + $query->select($fields); + } + } + + $options = $this->_setFieldMatchers( + $options, + ['keyField', 'valueField', 'groupField'] + ); + + return $query->formatResults(function ($results) use ($options) { + /** @var \Cake\Collection\CollectionInterface $results */ + return $results->combine( + $options['keyField'], + $options['valueField'], + $options['groupField'] + ); + }); + } + + /** + * Results for this finder will be a nested array, and is appropriate if you want + * to use the parent_id field of your model data to build nested results. + * + * Values belonging to a parent row based on their parent_id value will be + * recursively nested inside the parent row values using the `children` property + * + * You can customize what fields are used for nesting results, by default the + * primary key and the `parent_id` fields are used. If you wish to change + * these defaults you need to provide the keys `keyField`, `parentField` or `nestingKey` in + * `$options`: + * + * ``` + * $table->find('threaded', [ + * 'keyField' => 'id', + * 'parentField' => 'ancestor_id' + * 'nestingKey' => 'children' + * ]); + * ``` + * + * @param \Cake\ORM\Query $query The query to find with + * @param array $options The options to find with + * @return \Cake\ORM\Query The query builder + */ + public function findThreaded(Query $query, array $options) + { + $options += [ + 'keyField' => $this->getPrimaryKey(), + 'parentField' => 'parent_id', + 'nestingKey' => 'children' + ]; + + if (isset($options['idField'])) { + $options['keyField'] = $options['idField']; + unset($options['idField']); + deprecationWarning('Option "idField" is deprecated, use "keyField" instead.'); + } + + $options = $this->_setFieldMatchers($options, ['keyField', 'parentField']); + + return $query->formatResults(function ($results) use ($options) { + /** @var \Cake\Collection\CollectionInterface $results */ + return $results->nest($options['keyField'], $options['parentField'], $options['nestingKey']); + }); + } + + /** + * Out of an options array, check if the keys described in `$keys` are arrays + * and change the values for closures that will concatenate the each of the + * properties in the value array when passed a row. + * + * This is an auxiliary function used for result formatters that can accept + * composite keys when comparing values. + * + * @param array $options the original options passed to a finder + * @param array $keys the keys to check in $options to build matchers from + * the associated value + * @return array + */ + protected function _setFieldMatchers($options, $keys) + { + foreach ($keys as $field) { + if (!is_array($options[$field])) { + continue; + } + + if (count($options[$field]) === 1) { + $options[$field] = current($options[$field]); + continue; + } + + $fields = $options[$field]; + $options[$field] = function ($row) use ($fields) { + $matches = []; + foreach ($fields as $field) { + $matches[] = $row[$field]; + } + + return implode(';', $matches); + }; + } + + return $options; + } + + /** + * {@inheritDoc} + * + * ### Usage + * + * Get an article and some relationships: + * + * ``` + * $article = $articles->get(1, ['contain' => ['Users', 'Comments']]); + * ``` + * + * @throws \Cake\Datasource\Exception\InvalidPrimaryKeyException When $primaryKey has an + * incorrect number of elements. + */ + public function get($primaryKey, $options = []) + { + $key = (array)$this->getPrimaryKey(); + $alias = $this->getAlias(); + foreach ($key as $index => $keyname) { + $key[$index] = $alias . '.' . $keyname; + } + $primaryKey = (array)$primaryKey; + if (count($key) !== count($primaryKey)) { + $primaryKey = $primaryKey ?: [null]; + $primaryKey = array_map(function ($key) { + return var_export($key, true); + }, $primaryKey); + + throw new InvalidPrimaryKeyException(sprintf( + 'Record not found in table "%s" with primary key [%s]', + $this->getTable(), + implode($primaryKey, ', ') + )); + } + $conditions = array_combine($key, $primaryKey); + + $cacheConfig = isset($options['cache']) ? $options['cache'] : false; + $cacheKey = isset($options['key']) ? $options['key'] : false; + $finder = isset($options['finder']) ? $options['finder'] : 'all'; + unset($options['key'], $options['cache'], $options['finder']); + + $query = $this->find($finder, $options)->where($conditions); + + if ($cacheConfig) { + if (!$cacheKey) { + $cacheKey = sprintf( + 'get:%s.%s%s', + $this->getConnection()->configName(), + $this->getTable(), + json_encode($primaryKey) + ); + } + $query->cache($cacheKey, $cacheConfig); + } + + return $query->firstOrFail(); + } + + /** + * Handles the logic executing of a worker inside a transaction. + * + * @param callable $worker The worker that will run inside the transaction. + * @param bool $atomic Whether to execute the worker inside a database transaction. + * @return mixed + */ + protected function _executeTransaction(callable $worker, $atomic = true) + { + if ($atomic) { + return $this->getConnection()->transactional(function () use ($worker) { + return $worker(); + }); + } + + return $worker(); + } + + /** + * Checks if the caller would have executed a commit on a transaction. + * + * @param bool $atomic True if an atomic transaction was used. + * @param bool $primary True if a primary was used. + * @return bool Returns true if a transaction was committed. + */ + protected function _transactionCommitted($atomic, $primary) + { + return !$this->getConnection()->inTransaction() && ($atomic || (!$atomic && $primary)); + } + + /** + * Finds an existing record or creates a new one. + * + * A find() will be done to locate an existing record using the attributes + * defined in $search. If records matches the conditions, the first record + * will be returned. + * + * If no record can be found, a new entity will be created + * with the $search properties. If a callback is provided, it will be + * called allowing you to define additional default values. The new + * entity will be saved and returned. + * + * If your find conditions require custom order, associations or conditions, then the $search + * parameter can be a callable that takes the Query as the argument, or a \Cake\ORM\Query object passed + * as the $search parameter. Allowing you to customize the find results. + * + * ### Options + * + * The options array is passed to the save method with exception to the following keys: + * + * - atomic: Whether to execute the methods for find, save and callbacks inside a database + * transaction (default: true) + * - defaults: Whether to use the search criteria as default values for the new entity (default: true) + * + * @param array|\Cake\ORM\Query $search The criteria to find existing + * records by. Note that when you pass a query object you'll have to use + * the 2nd arg of the method to modify the entity data before saving. + * @param callable|null $callback A callback that will be invoked for newly + * created entities. This callback will be called *before* the entity + * is persisted. + * @param array $options The options to use when saving. + * @return \Cake\Datasource\EntityInterface An entity. + */ + public function findOrCreate($search, callable $callback = null, $options = []) + { + $options = new ArrayObject($options + [ + 'atomic' => true, + 'defaults' => true, + ]); + + $entity = $this->_executeTransaction(function () use ($search, $callback, $options) { + return $this->_processFindOrCreate($search, $callback, $options->getArrayCopy()); + }, $options['atomic']); + + if ($entity && $this->_transactionCommitted($options['atomic'], true)) { + $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options')); + } + + return $entity; + } + + /** + * Performs the actual find and/or create of an entity based on the passed options. + * + * @param array|callable $search The criteria to find an existing record by, or a callable tha will + * customize the find query. + * @param callable|null $callback A callback that will be invoked for newly + * created entities. This callback will be called *before* the entity + * is persisted. + * @param array $options The options to use when saving. + * @return \Cake\Datasource\EntityInterface An entity. + */ + protected function _processFindOrCreate($search, callable $callback = null, $options = []) + { + if (is_callable($search)) { + $query = $this->find(); + $search($query); + } elseif (is_array($search)) { + $query = $this->find()->where($search); + } elseif ($search instanceof Query) { + $query = $search; + } else { + throw new InvalidArgumentException('Search criteria must be an array, callable or Query'); + } + $row = $query->first(); + if ($row !== null) { + return $row; + } + $entity = $this->newEntity(); + if ($options['defaults'] && is_array($search)) { + $entity->set($search, ['guard' => false]); + } + if ($callback !== null) { + $entity = $callback($entity) ?: $entity; + } + unset($options['defaults']); + + return $this->save($entity, $options) ?: $entity; + } + + /** + * Gets the query object for findOrCreate(). + * + * @param array|\Cake\ORM\Query|string $search The criteria to find existing records by. + * @return \Cake\ORM\Query + */ + protected function _getFindOrCreateQuery($search) + { + if ($search instanceof Query) { + return $search; + } + + return $this->find()->where($search); + } + + /** + * Creates a new Query instance for a table. + * + * @return \Cake\ORM\Query + */ + public function query() + { + return new Query($this->getConnection(), $this); + } + + /** + * {@inheritDoc} + */ + public function updateAll($fields, $conditions) + { + $query = $this->query(); + $query->update() + ->set($fields) + ->where($conditions); + $statement = $query->execute(); + $statement->closeCursor(); + + return $statement->rowCount(); + } + + /** + * {@inheritDoc} + */ + public function deleteAll($conditions) + { + $query = $this->query() + ->delete() + ->where($conditions); + $statement = $query->execute(); + $statement->closeCursor(); + + return $statement->rowCount(); + } + + /** + * {@inheritDoc} + */ + public function exists($conditions) + { + return (bool)count( + $this->find('all') + ->select(['existing' => 1]) + ->where($conditions) + ->limit(1) + ->enableHydration(false) + ->toArray() + ); + } + + /** + * {@inheritDoc} + * + * ### Options + * + * The options array accepts the following keys: + * + * - atomic: Whether to execute the save and callbacks inside a database + * transaction (default: true) + * - checkRules: Whether or not to check the rules on entity before saving, if the checking + * fails, it will abort the save operation. (default:true) + * - associated: If `true` it will save 1st level associated entities as they are found + * in the passed `$entity` whenever the property defined for the association + * is marked as dirty. If an array, it will be interpreted as the list of associations + * to be saved. It is possible to provide different options for saving on associated + * table objects using this key by making the custom options the array value. + * If `false` no associated records will be saved. (default: `true`) + * - checkExisting: Whether or not to check if the entity already exists, assuming that the + * entity is marked as not new, and the primary key has been set. + * + * ### Events + * + * When saving, this method will trigger four events: + * + * - Model.beforeRules: Will be triggered right before any rule checking is done + * for the passed entity if the `checkRules` key in $options is not set to false. + * Listeners will receive as arguments the entity, options array and the operation type. + * If the event is stopped the rules check result will be set to the result of the event itself. + * - Model.afterRules: Will be triggered right after the `checkRules()` method is + * called for the entity. Listeners will receive as arguments the entity, + * options array, the result of checking the rules and the operation type. + * If the event is stopped the checking result will be set to the result of + * the event itself. + * - Model.beforeSave: Will be triggered just before the list of fields to be + * persisted is calculated. It receives both the entity and the options as + * arguments. The options array is passed as an ArrayObject, so any changes in + * it will be reflected in every listener and remembered at the end of the event + * so it can be used for the rest of the save operation. Returning false in any + * of the listeners will abort the saving process. If the event is stopped + * using the event API, the event object's `result` property will be returned. + * This can be useful when having your own saving strategy implemented inside a + * listener. + * - Model.afterSave: Will be triggered after a successful insert or save, + * listeners will receive the entity and the options array as arguments. The type + * of operation performed (insert or update) can be determined by checking the + * entity's method `isNew`, true meaning an insert and false an update. + * - Model.afterSaveCommit: Will be triggered after the transaction is commited + * for atomic save, listeners will receive the entity and the options array + * as arguments. + * + * This method will determine whether the passed entity needs to be + * inserted or updated in the database. It does that by checking the `isNew` + * method on the entity. If the entity to be saved returns a non-empty value from + * its `errors()` method, it will not be saved. + * + * ### Saving on associated tables + * + * This method will by default persist entities belonging to associated tables, + * whenever a dirty property matching the name of the property name set for an + * association in this table. It is possible to control what associations will + * be saved and to pass additional option for saving them. + * + * ``` + * // Only save the comments association + * $articles->save($entity, ['associated' => ['Comments']]); + * + * // Save the company, the employees and related addresses for each of them. + * // For employees do not check the entity rules + * $companies->save($entity, [ + * 'associated' => [ + * 'Employees' => [ + * 'associated' => ['Addresses'], + * 'checkRules' => false + * ] + * ] + * ]); + * + * // Save no associations + * $articles->save($entity, ['associated' => false]); + * ``` + * + * @param \Cake\Datasource\EntityInterface $entity + * @param array $options + * @return \Cake\Datasource\EntityInterface|false + * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction is aborted in the afterSave event. + */ + public function save(EntityInterface $entity, $options = []) + { + if ($options instanceof SaveOptionsBuilder) { + $options = $options->toArray(); + } + + $options = new ArrayObject($options + [ + 'atomic' => true, + 'associated' => true, + 'checkRules' => true, + 'checkExisting' => true, + '_primary' => true + ]); + + if ($entity->getErrors()) { + return false; + } + + if ($entity->isNew() === false && !$entity->isDirty()) { + return $entity; + } + + $success = $this->_executeTransaction(function () use ($entity, $options) { + return $this->_processSave($entity, $options); + }, $options['atomic']); + + if ($success) { + if ($this->_transactionCommitted($options['atomic'], $options['_primary'])) { + $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options')); + } + if ($options['atomic'] || $options['_primary']) { + $entity->clean(); + $entity->isNew(false); + $entity->setSource($this->getRegistryAlias()); + } + } + + return $success; + } + + /** + * Try to save an entity or throw a PersistenceFailedException if the application rules checks failed, + * the entity contains errors or the save was aborted by a callback. + * + * @param \Cake\Datasource\EntityInterface $entity the entity to be saved + * @param array|\ArrayAccess $options The options to use when saving. + * @return \Cake\Datasource\EntityInterface + * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved + * @see \Cake\ORM\Table::save() + */ + public function saveOrFail(EntityInterface $entity, $options = []) + { + $saved = $this->save($entity, $options); + if ($saved === false) { + throw new PersistenceFailedException($entity, ['save']); + } + + return $saved; + } + + /** + * Performs the actual saving of an entity based on the passed options. + * + * @param \Cake\Datasource\EntityInterface $entity the entity to be saved + * @param \ArrayObject $options the options to use for the save operation + * @return \Cake\Datasource\EntityInterface|bool + * @throws \RuntimeException When an entity is missing some of the primary keys. + * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction + * is aborted in the afterSave event. + */ + protected function _processSave($entity, $options) + { + $primaryColumns = (array)$this->getPrimaryKey(); + + if ($options['checkExisting'] && $primaryColumns && $entity->isNew() && $entity->has($primaryColumns)) { + $alias = $this->getAlias(); + $conditions = []; + foreach ($entity->extract($primaryColumns) as $k => $v) { + $conditions["$alias.$k"] = $v; + } + $entity->isNew(!$this->exists($conditions)); + } + + $mode = $entity->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE; + if ($options['checkRules'] && !$this->checkRules($entity, $mode, $options)) { + return false; + } + + $options['associated'] = $this->_associations->normalizeKeys($options['associated']); + $event = $this->dispatchEvent('Model.beforeSave', compact('entity', 'options')); + + if ($event->isStopped()) { + return $event->getResult(); + } + + $saved = $this->_associations->saveParents( + $this, + $entity, + $options['associated'], + ['_primary' => false] + $options->getArrayCopy() + ); + + if (!$saved && $options['atomic']) { + return false; + } + + $data = $entity->extract($this->getSchema()->columns(), true); + $isNew = $entity->isNew(); + + if ($isNew) { + $success = $this->_insert($entity, $data); + } else { + $success = $this->_update($entity, $data); + } + + if ($success) { + $success = $this->_onSaveSuccess($entity, $options); + } + + if (!$success && $isNew) { + $entity->unsetProperty($this->getPrimaryKey()); + $entity->isNew(true); + } + + return $success ? $entity : false; + } + + /** + * Handles the saving of children associations and executing the afterSave logic + * once the entity for this table has been saved successfully. + * + * @param \Cake\Datasource\EntityInterface $entity the entity to be saved + * @param \ArrayObject $options the options to use for the save operation + * @return bool True on success + * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction + * is aborted in the afterSave event. + */ + protected function _onSaveSuccess($entity, $options) + { + $success = $this->_associations->saveChildren( + $this, + $entity, + $options['associated'], + ['_primary' => false] + $options->getArrayCopy() + ); + + if (!$success && $options['atomic']) { + return false; + } + + $this->dispatchEvent('Model.afterSave', compact('entity', 'options')); + + if ($options['atomic'] && !$this->getConnection()->inTransaction()) { + throw new RolledbackTransactionException(['table' => get_class($this)]); + } + + if (!$options['atomic'] && !$options['_primary']) { + $entity->clean(); + $entity->isNew(false); + $entity->setSource($this->getRegistryAlias()); + } + + return true; + } + + /** + * Auxiliary function to handle the insert of an entity's data in the table + * + * @param \Cake\Datasource\EntityInterface $entity the subject entity from were $data was extracted + * @param array $data The actual data that needs to be saved + * @return \Cake\Datasource\EntityInterface|bool + * @throws \RuntimeException if not all the primary keys where supplied or could + * be generated when the table has composite primary keys. Or when the table has no primary key. + */ + protected function _insert($entity, $data) + { + $primary = (array)$this->getPrimaryKey(); + if (empty($primary)) { + $msg = sprintf( + 'Cannot insert row in "%s" table, it has no primary key.', + $this->getTable() + ); + throw new RuntimeException($msg); + } + $keys = array_fill(0, count($primary), null); + $id = (array)$this->_newId($primary) + $keys; + + // Generate primary keys preferring values in $data. + $primary = array_combine($primary, $id); + $primary = array_intersect_key($data, $primary) + $primary; + + $filteredKeys = array_filter($primary, 'strlen'); + $data += $filteredKeys; + + if (count($primary) > 1) { + $schema = $this->getSchema(); + foreach ($primary as $k => $v) { + if (!isset($data[$k]) && empty($schema->getColumn($k)['autoIncrement'])) { + $msg = 'Cannot insert row, some of the primary key values are missing. '; + $msg .= sprintf( + 'Got (%s), expecting (%s)', + implode(', ', $filteredKeys + $entity->extract(array_keys($primary))), + implode(', ', array_keys($primary)) + ); + throw new RuntimeException($msg); + } + } + } + + $success = false; + if (empty($data)) { + return $success; + } + + $statement = $this->query()->insert(array_keys($data)) + ->values($data) + ->execute(); + + if ($statement->rowCount() !== 0) { + $success = $entity; + $entity->set($filteredKeys, ['guard' => false]); + $schema = $this->getSchema(); + $driver = $this->getConnection()->getDriver(); + foreach ($primary as $key => $v) { + if (!isset($data[$key])) { + $id = $statement->lastInsertId($this->getTable(), $key); + $type = $schema->getColumnType($key); + $entity->set($key, Type::build($type)->toPHP($id, $driver)); + break; + } + } + } + $statement->closeCursor(); + + return $success; + } + + /** + * Generate a primary key value for a new record. + * + * By default, this uses the type system to generate a new primary key + * value if possible. You can override this method if you have specific requirements + * for id generation. + * + * Note: The ORM will not generate primary key values for composite primary keys. + * You can overwrite _newId() in your table class. + * + * @param array $primary The primary key columns to get a new ID for. + * @return null|string|array Either null or the primary key value or a list of primary key values. + */ + protected function _newId($primary) + { + if (!$primary || count((array)$primary) > 1) { + return null; + } + $typeName = $this->getSchema()->getColumnType($primary[0]); + $type = Type::build($typeName); + + return $type->newId(); + } + + /** + * Auxiliary function to handle the update of an entity's data in the table + * + * @param \Cake\Datasource\EntityInterface $entity the subject entity from were $data was extracted + * @param array $data The actual data that needs to be saved + * @return \Cake\Datasource\EntityInterface|bool + * @throws \InvalidArgumentException When primary key data is missing. + */ + protected function _update($entity, $data) + { + $primaryColumns = (array)$this->getPrimaryKey(); + $primaryKey = $entity->extract($primaryColumns); + + $data = array_diff_key($data, $primaryKey); + if (empty($data)) { + return $entity; + } + + if (count($primaryColumns) === 0) { + $entityClass = get_class($entity); + $table = $this->getTable(); + $message = "Cannot update `$entityClass`. The `$table` has no primary key."; + throw new InvalidArgumentException($message); + } + + if (!$entity->has($primaryColumns)) { + $message = 'All primary key value(s) are needed for updating, '; + $message .= get_class($entity) . ' is missing ' . implode(', ', $primaryColumns); + throw new InvalidArgumentException($message); + } + + $query = $this->query(); + $statement = $query->update() + ->set($data) + ->where($primaryKey) + ->execute(); + + $success = false; + if ($statement->errorCode() === '00000') { + $success = $entity; + } + $statement->closeCursor(); + + return $success; + } + + /** + * Persists multiple entities of a table. + * + * The records will be saved in a transaction which will be rolled back if + * any one of the records fails to save due to failed validation or database + * error. + * + * @param \Cake\Datasource\EntityInterface[]|\Cake\ORM\ResultSet $entities Entities to save. + * @param array|\ArrayAccess $options Options used when calling Table::save() for each entity. + * @return bool|\Cake\Datasource\EntityInterface[]|\Cake\ORM\ResultSet False on failure, entities list on success. + */ + public function saveMany($entities, $options = []) + { + $isNew = []; + $cleanup = function ($entities) use (&$isNew) { + foreach ($entities as $key => $entity) { + if (isset($isNew[$key]) && $isNew[$key]) { + $entity->unsetProperty($this->getPrimaryKey()); + $entity->isNew(true); + } + } + }; + + try { + $return = $this->getConnection() + ->transactional(function () use ($entities, $options, &$isNew) { + foreach ($entities as $key => $entity) { + $isNew[$key] = $entity->isNew(); + if ($this->save($entity, $options) === false) { + return false; + } + } + }); + } catch (\Exception $e) { + $cleanup($entities); + + throw $e; + } + + if ($return === false) { + $cleanup($entities); + + return false; + } + + return $entities; + } + + /** + * {@inheritDoc} + * + * For HasMany and HasOne associations records will be removed based on + * the dependent option. Join table records in BelongsToMany associations + * will always be removed. You can use the `cascadeCallbacks` option + * when defining associations to change how associated data is deleted. + * + * ### Options + * + * - `atomic` Defaults to true. When true the deletion happens within a transaction. + * - `checkRules` Defaults to true. Check deletion rules before deleting the record. + * + * ### Events + * + * - `Model.beforeDelete` Fired before the delete occurs. If stopped the delete + * will be aborted. Receives the event, entity, and options. + * - `Model.afterDelete` Fired after the delete has been successful. Receives + * the event, entity, and options. + * - `Model.afterDeleteCommit` Fired after the transaction is committed for + * an atomic delete. Receives the event, entity, and options. + * + * The options argument will be converted into an \ArrayObject instance + * for the duration of the callbacks, this allows listeners to modify + * the options used in the delete operation. + * + */ + public function delete(EntityInterface $entity, $options = []) + { + $options = new ArrayObject($options + [ + 'atomic' => true, + 'checkRules' => true, + '_primary' => true, + ]); + + $success = $this->_executeTransaction(function () use ($entity, $options) { + return $this->_processDelete($entity, $options); + }, $options['atomic']); + + if ($success && $this->_transactionCommitted($options['atomic'], $options['_primary'])) { + $this->dispatchEvent('Model.afterDeleteCommit', [ + 'entity' => $entity, + 'options' => $options + ]); + } + + return $success; + } + + /** + * Try to delete an entity or throw a PersistenceFailedException if the entity is new, + * has no primary key value, application rules checks failed or the delete was aborted by a callback. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to remove. + * @param array|\ArrayAccess $options The options for the delete. + * @return bool success + * @throws \Cake\ORM\Exception\PersistenceFailedException + * @see \Cake\ORM\Table::delete() + */ + public function deleteOrFail(EntityInterface $entity, $options = []) + { + $deleted = $this->delete($entity, $options); + if ($deleted === false) { + throw new PersistenceFailedException($entity, ['delete']); + } + + return $deleted; + } + + /** + * Perform the delete operation. + * + * Will delete the entity provided. Will remove rows from any + * dependent associations, and clear out join tables for BelongsToMany associations. + * + * @param \Cake\Datasource\EntityInterface $entity The entity to delete. + * @param \ArrayObject $options The options for the delete. + * @throws \InvalidArgumentException if there are no primary key values of the + * passed entity + * @return bool success + */ + protected function _processDelete($entity, $options) + { + if ($entity->isNew()) { + return false; + } + + $primaryKey = (array)$this->getPrimaryKey(); + if (!$entity->has($primaryKey)) { + $msg = 'Deleting requires all primary key values.'; + throw new InvalidArgumentException($msg); + } + + if ($options['checkRules'] && !$this->checkRules($entity, RulesChecker::DELETE, $options)) { + return false; + } + + $event = $this->dispatchEvent('Model.beforeDelete', [ + 'entity' => $entity, + 'options' => $options + ]); + + if ($event->isStopped()) { + return $event->getResult(); + } + + $this->_associations->cascadeDelete( + $entity, + ['_primary' => false] + $options->getArrayCopy() + ); + + $query = $this->query(); + $conditions = (array)$entity->extract($primaryKey); + $statement = $query->delete() + ->where($conditions) + ->execute(); + + $success = $statement->rowCount() > 0; + if (!$success) { + return $success; + } + + $this->dispatchEvent('Model.afterDelete', [ + 'entity' => $entity, + 'options' => $options + ]); + + return $success; + } + + /** + * Returns true if the finder exists for the table + * + * @param string $type name of finder to check + * + * @return bool + */ + public function hasFinder($type) + { + $finder = 'find' . $type; + + return method_exists($this, $finder) || ($this->_behaviors && $this->_behaviors->hasFinder($type)); + } + + /** + * Calls a finder method directly and applies it to the passed query, + * if no query is passed a new one will be created and returned + * + * @param string $type name of the finder to be called + * @param \Cake\ORM\Query $query The query object to apply the finder options to + * @param array $options List of options to pass to the finder + * @return \Cake\ORM\Query + * @throws \BadMethodCallException + */ + public function callFinder($type, Query $query, array $options = []) + { + $query->applyOptions($options); + $options = $query->getOptions(); + $finder = 'find' . $type; + if (method_exists($this, $finder)) { + return $this->{$finder}($query, $options); + } + + if ($this->_behaviors && $this->_behaviors->hasFinder($type)) { + return $this->_behaviors->callFinder($type, [$query, $options]); + } + + throw new BadMethodCallException( + sprintf('Unknown finder method "%s"', $type) + ); + } + + /** + * Provides the dynamic findBy and findByAll methods. + * + * @param string $method The method name that was fired. + * @param array $args List of arguments passed to the function. + * @return mixed + * @throws \BadMethodCallException when there are missing arguments, or when + * and & or are combined. + */ + protected function _dynamicFinder($method, $args) + { + $method = Inflector::underscore($method); + preg_match('/^find_([\w]+)_by_/', $method, $matches); + if (empty($matches)) { + // find_by_ is 8 characters. + $fields = substr($method, 8); + $findType = 'all'; + } else { + $fields = substr($method, strlen($matches[0])); + $findType = Inflector::variable($matches[1]); + } + $hasOr = strpos($fields, '_or_'); + $hasAnd = strpos($fields, '_and_'); + + $makeConditions = function ($fields, $args) { + $conditions = []; + if (count($args) < count($fields)) { + throw new BadMethodCallException(sprintf( + 'Not enough arguments for magic finder. Got %s required %s', + count($args), + count($fields) + )); + } + foreach ($fields as $field) { + $conditions[$this->aliasField($field)] = array_shift($args); + } + + return $conditions; + }; + + if ($hasOr !== false && $hasAnd !== false) { + throw new BadMethodCallException( + 'Cannot mix "and" & "or" in a magic finder. Use find() instead.' + ); + } + + $conditions = []; + if ($hasOr === false && $hasAnd === false) { + $conditions = $makeConditions([$fields], $args); + } elseif ($hasOr !== false) { + $fields = explode('_or_', $fields); + $conditions = [ + 'OR' => $makeConditions($fields, $args) + ]; + } elseif ($hasAnd !== false) { + $fields = explode('_and_', $fields); + $conditions = $makeConditions($fields, $args); + } + + return $this->find($findType, [ + 'conditions' => $conditions, + ]); + } + + /** + * Handles behavior delegation + dynamic finders. + * + * If your Table uses any behaviors you can call them as if + * they were on the table object. + * + * @param string $method name of the method to be invoked + * @param array $args List of arguments passed to the function + * @return mixed + * @throws \BadMethodCallException + */ + public function __call($method, $args) + { + if ($this->_behaviors && $this->_behaviors->hasMethod($method)) { + return $this->_behaviors->call($method, $args); + } + if (preg_match('/^find(?:\w+)?By/', $method) > 0) { + return $this->_dynamicFinder($method, $args); + } + + throw new BadMethodCallException( + sprintf('Unknown method "%s"', $method) + ); + } + + /** + * Returns the association named after the passed value if exists, otherwise + * throws an exception. + * + * @param string $property the association name + * @return \Cake\ORM\Association + * @throws \RuntimeException if no association with such name exists + */ + public function __get($property) + { + $association = $this->_associations->get($property); + if (!$association) { + throw new RuntimeException(sprintf( + 'Table "%s" is not associated with "%s"', + get_class($this), + $property + )); + } + + return $association; + } + + /** + * Returns whether an association named after the passed value + * exists for this table. + * + * @param string $property the association name + * @return bool + */ + public function __isset($property) + { + return $this->_associations->has($property); + } + + /** + * Get the object used to marshal/convert array data into objects. + * + * Override this method if you want a table object to use custom + * marshalling logic. + * + * @return \Cake\ORM\Marshaller + * @see \Cake\ORM\Marshaller + */ + public function marshaller() + { + return new Marshaller($this); + } + + /** + * {@inheritDoc} + * + * By default all the associations on this table will be hydrated. You can + * limit which associations are built, or include deeper associations + * using the options parameter: + * + * ``` + * $article = $this->Articles->newEntity( + * $this->request->getData(), + * ['associated' => ['Tags', 'Comments.Users']] + * ); + * ``` + * + * You can limit fields that will be present in the constructed entity by + * passing the `fields` option, which is also accepted for associations: + * + * ``` + * $article = $this->Articles->newEntity($this->request->getData(), [ + * 'fields' => ['title', 'body', 'tags', 'comments'], + * 'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']] + * ] + * ); + * ``` + * + * The `fields` option lets remove or restrict input data from ending up in + * the entity. If you'd like to relax the entity's default accessible fields, + * you can use the `accessibleFields` option: + * + * ``` + * $article = $this->Articles->newEntity( + * $this->request->getData(), + * ['accessibleFields' => ['protected_field' => true]] + * ); + * ``` + * + * By default, the data is validated before being passed to the new entity. In + * the case of invalid fields, those will not be present in the resulting object. + * The `validate` option can be used to disable validation on the passed data: + * + * ``` + * $article = $this->Articles->newEntity( + * $this->request->getData(), + * ['validate' => false] + * ); + * ``` + * + * You can also pass the name of the validator to use in the `validate` option. + * If `null` is passed to the first param of this function, no validation will + * be performed. + * + * You can use the `Model.beforeMarshal` event to modify request data + * before it is converted into entities. + */ + public function newEntity($data = null, array $options = []) + { + if ($data === null) { + $class = $this->getEntityClass(); + + return new $class([], ['source' => $this->getRegistryAlias()]); + } + if (!isset($options['associated'])) { + $options['associated'] = $this->_associations->keys(); + } + $marshaller = $this->marshaller(); + + return $marshaller->one($data, $options); + } + + /** + * {@inheritDoc} + * + * By default all the associations on this table will be hydrated. You can + * limit which associations are built, or include deeper associations + * using the options parameter: + * + * ``` + * $articles = $this->Articles->newEntities( + * $this->request->getData(), + * ['associated' => ['Tags', 'Comments.Users']] + * ); + * ``` + * + * You can limit fields that will be present in the constructed entities by + * passing the `fields` option, which is also accepted for associations: + * + * ``` + * $articles = $this->Articles->newEntities($this->request->getData(), [ + * 'fields' => ['title', 'body', 'tags', 'comments'], + * 'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']] + * ] + * ); + * ``` + * + * You can use the `Model.beforeMarshal` event to modify request data + * before it is converted into entities. + */ + public function newEntities(array $data, array $options = []) + { + if (!isset($options['associated'])) { + $options['associated'] = $this->_associations->keys(); + } + $marshaller = $this->marshaller(); + + return $marshaller->many($data, $options); + } + + /** + * {@inheritDoc} + * + * When merging HasMany or BelongsToMany associations, all the entities in the + * `$data` array will appear, those that can be matched by primary key will get + * the data merged, but those that cannot, will be discarded. + * + * You can limit fields that will be present in the merged entity by + * passing the `fields` option, which is also accepted for associations: + * + * ``` + * $article = $this->Articles->patchEntity($article, $this->request->getData(), [ + * 'fields' => ['title', 'body', 'tags', 'comments'], + * 'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']] + * ] + * ); + * ``` + * + * By default, the data is validated before being passed to the entity. In + * the case of invalid fields, those will not be assigned to the entity. + * The `validate` option can be used to disable validation on the passed data: + * + * ``` + * $article = $this->patchEntity($article, $this->request->getData(),[ + * 'validate' => false + * ]); + * ``` + * + * You can use the `Model.beforeMarshal` event to modify request data + * before it is converted into entities. + * + * When patching scalar values (null/booleans/string/integer/float), if the property + * presently has an identical value, the setter will not be called, and the + * property will not be marked as dirty. This is an optimization to prevent unnecessary field + * updates when persisting entities. + */ + public function patchEntity(EntityInterface $entity, array $data, array $options = []) + { + if (!isset($options['associated'])) { + $options['associated'] = $this->_associations->keys(); + } + $marshaller = $this->marshaller(); + + return $marshaller->merge($entity, $data, $options); + } + + /** + * {@inheritDoc} + * + * Those entries in `$entities` that cannot be matched to any record in + * `$data` will be discarded. Records in `$data` that could not be matched will + * be marshalled as a new entity. + * + * When merging HasMany or BelongsToMany associations, all the entities in the + * `$data` array will appear, those that can be matched by primary key will get + * the data merged, but those that cannot, will be discarded. + * + * You can limit fields that will be present in the merged entities by + * passing the `fields` option, which is also accepted for associations: + * + * ``` + * $articles = $this->Articles->patchEntities($articles, $this->request->getData(), [ + * 'fields' => ['title', 'body', 'tags', 'comments'], + * 'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']] + * ] + * ); + * ``` + * + * You can use the `Model.beforeMarshal` event to modify request data + * before it is converted into entities. + */ + public function patchEntities($entities, array $data, array $options = []) + { + if (!isset($options['associated'])) { + $options['associated'] = $this->_associations->keys(); + } + $marshaller = $this->marshaller(); + + return $marshaller->mergeMany($entities, $data, $options); + } + + /** + * Validator method used to check the uniqueness of a value for a column. + * This is meant to be used with the validation API and not to be called + * directly. + * + * ### Example: + * + * ``` + * $validator->add('email', [ + * 'unique' => ['rule' => 'validateUnique', 'provider' => 'table'] + * ]) + * ``` + * + * Unique validation can be scoped to the value of another column: + * + * ``` + * $validator->add('email', [ + * 'unique' => [ + * 'rule' => ['validateUnique', ['scope' => 'site_id']], + * 'provider' => 'table' + * ] + * ]); + * ``` + * + * In the above example, the email uniqueness will be scoped to only rows having + * the same site_id. Scoping will only be used if the scoping field is present in + * the data to be validated. + * + * @param mixed $value The value of column to be checked for uniqueness. + * @param array $options The options array, optionally containing the 'scope' key. + * May also be the validation context, if there are no options. + * @param array|null $context Either the validation context or null. + * @return bool True if the value is unique, or false if a non-scalar, non-unique value was given. + */ + public function validateUnique($value, array $options, array $context = null) + { + if ($context === null) { + $context = $options; + } + $entity = new Entity( + $context['data'], + [ + 'useSetters' => false, + 'markNew' => $context['newRecord'], + 'source' => $this->getRegistryAlias() + ] + ); + $fields = array_merge( + [$context['field']], + isset($options['scope']) ? (array)$options['scope'] : [] + ); + $values = $entity->extract($fields); + foreach ($values as $field) { + if ($field !== null && !is_scalar($field)) { + return false; + } + } + $rule = new IsUnique($fields, $options); + + return $rule($entity, ['repository' => $this]); + } + + /** + * Get the Model callbacks this table is interested in. + * + * By implementing the conventional methods a table class is assumed + * to be interested in the related event. + * + * Override this method if you need to add non-conventional event listeners. + * Or if you want you table to listen to non-standard events. + * + * The conventional method map is: + * + * - Model.beforeMarshal => beforeMarshal + * - Model.buildValidator => buildValidator + * - Model.beforeFind => beforeFind + * - Model.beforeSave => beforeSave + * - Model.afterSave => afterSave + * - Model.afterSaveCommit => afterSaveCommit + * - Model.beforeDelete => beforeDelete + * - Model.afterDelete => afterDelete + * - Model.afterDeleteCommit => afterDeleteCommit + * - Model.beforeRules => beforeRules + * - Model.afterRules => afterRules + * + * @return array + */ + public function implementedEvents() + { + $eventMap = [ + 'Model.beforeMarshal' => 'beforeMarshal', + 'Model.buildValidator' => 'buildValidator', + 'Model.beforeFind' => 'beforeFind', + 'Model.beforeSave' => 'beforeSave', + 'Model.afterSave' => 'afterSave', + 'Model.afterSaveCommit' => 'afterSaveCommit', + 'Model.beforeDelete' => 'beforeDelete', + 'Model.afterDelete' => 'afterDelete', + 'Model.afterDeleteCommit' => 'afterDeleteCommit', + 'Model.beforeRules' => 'beforeRules', + 'Model.afterRules' => 'afterRules', + ]; + $events = []; + + foreach ($eventMap as $event => $method) { + if (!method_exists($this, $method)) { + continue; + } + $events[$event] = $method; + } + + return $events; + } + + /** + * {@inheritDoc} + * + * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. + * @return \Cake\ORM\RulesChecker + */ + public function buildRules(RulesChecker $rules) + { + return $rules; + } + + /** + * Gets a SaveOptionsBuilder instance. + * + * @param array $options Options to parse by the builder. + * @return \Cake\ORM\SaveOptionsBuilder + */ + public function getSaveOptionsBuilder(array $options = []) + { + return new SaveOptionsBuilder($this, $options); + } + + /** + * Loads the specified associations in the passed entity or list of entities + * by executing extra queries in the database and merging the results in the + * appropriate properties. + * + * ### Example: + * + * ``` + * $user = $usersTable->get(1); + * $user = $usersTable->loadInto($user, ['Articles.Tags', 'Articles.Comments']); + * echo $user->articles[0]->title; + * ``` + * + * You can also load associations for multiple entities at once + * + * ### Example: + * + * ``` + * $users = $usersTable->find()->where([...])->toList(); + * $users = $usersTable->loadInto($users, ['Articles.Tags', 'Articles.Comments']); + * echo $user[1]->articles[0]->title; + * ``` + * + * The properties for the associations to be loaded will be overwritten on each entity. + * + * @param \Cake\Datasource\EntityInterface|array $entities a single entity or list of entities + * @param array $contain A `contain()` compatible array. + * @see \Cake\ORM\Query::contain() + * @return \Cake\Datasource\EntityInterface|array + */ + public function loadInto($entities, array $contain) + { + return (new LazyEagerLoader)->loadInto($entities, $contain, $this); + } + + /** + * {@inheritDoc} + */ + protected function validationMethodExists($method) + { + return method_exists($this, $method) || $this->behaviors()->hasMethod($method); + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + $conn = $this->getConnection(); + $associations = $this->_associations; + $behaviors = $this->_behaviors; + + return [ + 'registryAlias' => $this->getRegistryAlias(), + 'table' => $this->getTable(), + 'alias' => $this->getAlias(), + 'entityClass' => $this->getEntityClass(), + 'associations' => $associations ? $associations->keys() : false, + 'behaviors' => $behaviors ? $behaviors->loaded() : false, + 'defaultConnection' => static::defaultConnectionName(), + 'connectionName' => $conn ? $conn->configName() : null + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/TableRegistry.php b/app/vendor/cakephp/cakephp/src/ORM/TableRegistry.php new file mode 100644 index 000000000..5c952687a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/TableRegistry.php @@ -0,0 +1,210 @@ + 'my_users']); + * ``` + * + * Configuration data is stored *per alias* if you use the same table with + * multiple aliases you will need to set configuration multiple times. + * + * ### Getting instances + * + * You can fetch instances out of the registry using get(). One instance is stored + * per alias. Once an alias is populated the same instance will always be returned. + * This is used to make the ORM use less memory and help make cyclic references easier + * to solve. + * + * ``` + * $table = TableRegistry::get('Users', $config); + * ``` + */ +class TableRegistry +{ + + /** + * LocatorInterface implementation instance. + * + * @var \Cake\ORM\Locator\LocatorInterface + */ + protected static $_locator; + + /** + * Default LocatorInterface implementation class. + * + * @var string + */ + protected static $_defaultLocatorClass = 'Cake\ORM\Locator\TableLocator'; + + /** + * Sets and returns a singleton instance of LocatorInterface implementation. + * + * @param \Cake\ORM\Locator\LocatorInterface|null $locator Instance of a locator to use. + * @return \Cake\ORM\Locator\LocatorInterface + * @deprecated 3.5.0 Use getTableLocator()/setTableLocator() instead. + */ + public static function locator(LocatorInterface $locator = null) + { + deprecationWarning( + 'TableRegistry::locator() is deprecated. ' . + 'Use setTableLocator()/getTableLocator() instead.' + ); + if ($locator) { + static::setTableLocator($locator); + } + + return static::getTableLocator(); + } + + /** + * Returns a singleton instance of LocatorInterface implementation. + * + * @return \Cake\ORM\Locator\LocatorInterface + */ + public static function getTableLocator() + { + if (!static::$_locator) { + static::$_locator = new static::$_defaultLocatorClass(); + } + + return static::$_locator; + } + + /** + * Sets singleton instance of LocatorInterface implementation. + * + * @param \Cake\ORM\Locator\LocatorInterface $tableLocator Instance of a locator to use. + * @return void + */ + public static function setTableLocator(LocatorInterface $tableLocator) + { + static::$_locator = $tableLocator; + } + + /** + * Stores a list of options to be used when instantiating an object + * with a matching alias. + * + * @param string|null $alias Name of the alias + * @param array|null $options list of options for the alias + * @return array The config data. + * @deprecated 3.6.0 Use \Cake\ORM\Locator\TableLocator::getConfig()/setConfig() instead. + */ + public static function config($alias = null, $options = null) + { + deprecationWarning( + 'TableRegistry::config() is deprecated. ' . + 'Use \Cake\ORM\Locator\TableLocator::getConfig()/setConfig() instead.' + ); + + return static::getTableLocator()->config($alias, $options); + } + + /** + * Get a table instance from the registry. + * + * See options specification in {@link TableLocator::get()}. + * + * @param string $alias The alias name you want to get. + * @param array $options The options you want to build the table with. + * @return \Cake\ORM\Table + * @deprecated 3.6.0 Use \Cake\ORM\Locator\TableLocator::get() instead. + */ + public static function get($alias, array $options = []) + { + return static::getTableLocator()->get($alias, $options); + } + + /** + * Check to see if an instance exists in the registry. + * + * @param string $alias The alias to check for. + * @return bool + * @deprecated 3.6.0 Use \Cake\ORM\Locator\TableLocator::exists() instead. + */ + public static function exists($alias) + { + return static::getTableLocator()->exists($alias); + } + + /** + * Set an instance. + * + * @param string $alias The alias to set. + * @param \Cake\ORM\Table $object The table to set. + * @return \Cake\ORM\Table + * @deprecated 3.6.0 Use \Cake\ORM\Locator\TableLocator::set() instead. + */ + public static function set($alias, Table $object) + { + return static::getTableLocator()->set($alias, $object); + } + + /** + * Removes an instance from the registry. + * + * @param string $alias The alias to remove. + * @return void + * @deprecated 3.6.0 Use \Cake\ORM\Locator\TableLocator::remove() instead. + */ + public static function remove($alias) + { + static::getTableLocator()->remove($alias); + } + + /** + * Clears the registry of configuration and instances. + * + * @return void + * @deprecated 3.6.0 Use \Cake\ORM\Locator\TableLocator::clear() instead. + */ + public static function clear() + { + static::getTableLocator()->clear(); + } + + /** + * Proxy for static calls on a locator. + * + * @param string $name Method name. + * @param array $arguments Method arguments. + * @return mixed + */ + public static function __callStatic($name, $arguments) + { + deprecationWarning( + 'TableRegistry::' . $name . '() is deprecated. ' . + 'Use \Cake\ORM\Locator\TableLocator::' . $name . '() instead.' + ); + + return static::getTableLocator()->$name(...$arguments); + } +} diff --git a/app/vendor/cakephp/cakephp/src/ORM/composer.json b/app/vendor/cakephp/cakephp/src/ORM/composer.json new file mode 100644 index 000000000..143289396 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/ORM/composer.json @@ -0,0 +1,43 @@ +{ + "name": "cakephp/orm", + "description": "CakePHP ORM - Provides a flexible and powerful ORM implementing a data-mapper pattern.", + "type": "library", + "keywords": [ + "cakephp", + "orm", + "data-mapper", + "data-mapper pattern" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/orm/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/orm" + }, + "require": { + "php": ">=5.6.0", + "cakephp/collection": "^3.6.0", + "cakephp/core": "^3.6.0", + "cakephp/datasource": "^3.6.0", + "cakephp/database": "^3.6.0", + "cakephp/event": "^3.6.0", + "cakephp/utility": "^3.6.0", + "cakephp/validation": "^3.6.0" + }, + "suggest": { + "cakephp/i18n": "If you are using Translate / Timestamp Behavior." + }, + "autoload": { + "psr-4": { + "Cake\\ORM\\": "." + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Dispatcher.php b/app/vendor/cakephp/cakephp/src/Routing/Dispatcher.php new file mode 100644 index 000000000..d1a3f2be4 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Dispatcher.php @@ -0,0 +1,99 @@ +getEventManager(), $this->_filters); + $response = $actionDispatcher->dispatch($request, $response); + if ($request->getParam('return', null) !== null) { + return $response->body(); + } + + return $response->send(); + } + + /** + * Add a filter to this dispatcher. + * + * The added filter will be attached to the event manager used + * by this dispatcher. + * + * @param \Cake\Event\EventListenerInterface $filter The filter to connect. Can be + * any EventListenerInterface. Typically an instance of \Cake\Routing\DispatcherFilter. + * @return void + */ + public function addFilter(EventListenerInterface $filter) + { + $this->_filters[] = $filter; + } + + /** + * Get the list of connected filters. + * + * @return \Cake\Event\EventListenerInterface[] + */ + public function filters() + { + return $this->_filters; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/DispatcherFactory.php b/app/vendor/cakephp/cakephp/src/Routing/DispatcherFactory.php new file mode 100644 index 000000000..91dc68620 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/DispatcherFactory.php @@ -0,0 +1,113 @@ +addFilter($middleware); + } + + return $dispatcher; + } + + /** + * Get the connected dispatcher filters. + * + * @return \Cake\Routing\DispatcherFilter[] + */ + public static function filters() + { + return static::$_stack; + } + + /** + * Clear the middleware stack. + * + * @return void + */ + public static function clear() + { + static::$_stack = []; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/DispatcherFilter.php b/app/vendor/cakephp/cakephp/src/Routing/DispatcherFilter.php new file mode 100644 index 000000000..56ba8daf0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/DispatcherFilter.php @@ -0,0 +1,217 @@ + '/blog']); + * ``` + * + * When the above filter is connected to a dispatcher it will only fire + * its `beforeDispatch` and `afterDispatch` methods on requests that start with `/blog`. + * + * The for condition can also be a regular expression by using the `preg:` prefix: + * + * ``` + * $filter = new BlogFilter(['for' => 'preg:#^/blog/\d+$#']); + * ``` + * + * ### Limiting filters based on conditions + * + * In addition to simple path based matching you can use a closure to match on arbitrary request + * or response conditions. For example: + * + * ``` + * $cookieMonster = new CookieFilter([ + * 'when' => function ($req, $res) { + * // Custom code goes here. + * } + * ]); + * ``` + * + * If your when condition returns `true` the before/after methods will be called. + * + * When using the `for` or `when` matchers, conditions will be re-checked on the before and after + * callback as the conditions could change during the dispatch cycle. + * + * @mixin \Cake\Core\InstanceConfigTrait + */ +class DispatcherFilter implements EventListenerInterface +{ + + use InstanceConfigTrait; + + /** + * Default priority for all methods in this filter + * + * @var int + */ + protected $_priority = 10; + + /** + * Default config + * + * These are merged with user-provided config when the class is used. + * The when and for options allow you to define conditions that are checked before + * your filter is called. + * + * @var array + */ + protected $_defaultConfig = [ + 'when' => null, + 'for' => null, + 'priority' => null, + ]; + + /** + * Constructor. + * + * @param array $config Settings for the filter. + * @throws \InvalidArgumentException When 'when' conditions are not callable. + */ + public function __construct($config = []) + { + if (!isset($config['priority'])) { + $config['priority'] = $this->_priority; + } + $this->setConfig($config); + if (isset($config['when']) && !is_callable($config['when'])) { + throw new InvalidArgumentException('"when" conditions must be a callable.'); + } + } + + /** + * Returns the list of events this filter listens to. + * Dispatcher notifies 2 different events `Dispatcher.before` and `Dispatcher.after`. + * By default this class will attach `preDispatch` and `postDispatch` method respectively. + * + * Override this method at will to only listen to the events you are interested in. + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Dispatcher.beforeDispatch' => [ + 'callable' => 'handle', + 'priority' => $this->_config['priority'] + ], + 'Dispatcher.afterDispatch' => [ + 'callable' => 'handle', + 'priority' => $this->_config['priority'] + ], + ]; + } + + /** + * Handler method that applies conditions and resolves the correct method to call. + * + * @param \Cake\Event\Event $event The event instance. + * @return mixed + */ + public function handle(Event $event) + { + $name = $event->getName(); + list(, $method) = explode('.', $name); + if (empty($this->_config['for']) && empty($this->_config['when'])) { + return $this->{$method}($event); + } + if ($this->matches($event)) { + return $this->{$method}($event); + } + } + + /** + * Check to see if the incoming request matches this filter's criteria. + * + * @param \Cake\Event\Event $event The event to match. + * @return bool + */ + public function matches(Event $event) + { + /* @var \Cake\Http\ServerRequest $request */ + $request = $event->getData('request'); + $pass = true; + if (!empty($this->_config['for'])) { + $len = strlen('preg:'); + $for = $this->_config['for']; + $url = $request->getRequestTarget(); + if (substr($for, 0, $len) === 'preg:') { + $pass = (bool)preg_match(substr($for, $len), $url); + } else { + $pass = strpos($url, $for) === 0; + } + } + if ($pass && !empty($this->_config['when'])) { + $response = $event->getData('response'); + $pass = $this->_config['when']($request, $response); + } + + return $pass; + } + + /** + * Method called before the controller is instantiated and called to serve a request. + * If used with default priority, it will be called after the Router has parsed the + * URL and set the routing params into the request object. + * + * If a Cake\Http\Response object instance is returned, it will be served at the end of the + * event cycle, not calling any controller as a result. This will also have the effect of + * not calling the after event in the dispatcher. + * + * If false is returned, the event will be stopped and no more listeners will be notified. + * Alternatively you can call `$event->stopPropagation()` to achieve the same result. + * + * @param \Cake\Event\Event $event container object having the `request`, `response` and `additionalParams` + * keys in the data property. + * @return void + */ + public function beforeDispatch(Event $event) + { + } + + /** + * Method called after the controller served a request and generated a response. + * It is possible to alter the response object at this point as it is not sent to the + * client yet. + * + * If false is returned, the event will be stopped and no more listeners will be notified. + * Alternatively you can call `$event->stopPropagation()` to achieve the same result. + * + * @param \Cake\Event\Event $event container object having the `request` and `response` + * keys in the data property. + * @return void + */ + public function afterDispatch(Event $event) + { + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Exception/DuplicateNamedRouteException.php b/app/vendor/cakephp/cakephp/src/Routing/Exception/DuplicateNamedRouteException.php new file mode 100644 index 000000000..4fbcbe83d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Exception/DuplicateNamedRouteException.php @@ -0,0 +1,38 @@ +_messageTemplate = $message['message']; + } + parent::__construct($message, $code, $previous); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Exception/MissingControllerException.php b/app/vendor/cakephp/cakephp/src/Routing/Exception/MissingControllerException.php new file mode 100644 index 000000000..5bd79f49a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Exception/MissingControllerException.php @@ -0,0 +1,36 @@ +_messageTemplate = $message['message']; + } elseif (isset($message['method']) && $message['method']) { + $this->_messageTemplate = $this->_messageTemplateWithMethod; + } + } + parent::__construct($message, $code, $previous); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Exception/RedirectException.php b/app/vendor/cakephp/cakephp/src/Routing/Exception/RedirectException.php new file mode 100644 index 000000000..2a392b95a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Exception/RedirectException.php @@ -0,0 +1,36 @@ +_cacheTime = $config['cacheTime']; + } + parent::__construct($config); + } + + /** + * Checks if a requested asset exists and sends it to the browser + * + * @param \Cake\Event\Event $event Event containing the request and response object + * @return \Cake\Http\Response|null If the client is requesting a recognized asset, null otherwise + * @throws \Cake\Http\Exception\NotFoundException When asset not found + */ + public function beforeDispatch(Event $event) + { + /* @var \Cake\Http\ServerRequest $request */ + $request = $event->getData('request'); + + $url = urldecode($request->getUri()->getPath()); + if (strpos($url, '..') !== false || strpos($url, '.') === false) { + return null; + } + + $assetFile = $this->_getAssetFile($url); + if ($assetFile === null || !file_exists($assetFile)) { + return null; + } + /* @var \Cake\Http\Response $response */ + $response = $event->getData('response'); + $event->stopPropagation(); + + $response = $response->withModified(filemtime($assetFile)); + if ($response->checkNotModified($request)) { + return $response; + } + + $pathSegments = explode('.', $url); + $ext = array_pop($pathSegments); + + return $this->_deliverAsset($request, $response, $assetFile, $ext); + } + + /** + * Builds asset file path based off url + * + * @param string $url Asset URL + * @return string Absolute path for asset file + */ + protected function _getAssetFile($url) + { + $parts = explode('/', ltrim($url, '/')); + $pluginPart = []; + for ($i = 0; $i < 2; $i++) { + if (!isset($parts[$i])) { + break; + } + $pluginPart[] = Inflector::camelize($parts[$i]); + $plugin = implode('/', $pluginPart); + if ($plugin && Plugin::loaded($plugin)) { + $parts = array_slice($parts, $i + 1); + $fileFragment = implode(DIRECTORY_SEPARATOR, $parts); + $pluginWebroot = Plugin::path($plugin) . 'webroot' . DIRECTORY_SEPARATOR; + + return $pluginWebroot . $fileFragment; + } + } + } + + /** + * Sends an asset file to the client + * + * @param \Cake\Http\ServerRequest $request The request object to use. + * @param \Cake\Http\Response $response The response object to use. + * @param string $assetFile Path to the asset file in the file system + * @param string $ext The extension of the file to determine its mime type + * @return \Cake\Http\Response The updated response. + */ + protected function _deliverAsset(ServerRequest $request, Response $response, $assetFile, $ext) + { + $compressionEnabled = $response->compress(); + if ($response->getType() === $ext) { + $contentType = 'application/octet-stream'; + $agent = $request->getEnv('HTTP_USER_AGENT'); + if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent) || preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) { + $contentType = 'application/octetstream'; + } + $response = $response->withType($contentType); + } + if (!$compressionEnabled) { + $response = $response->withHeader('Content-Length', filesize($assetFile)); + } + $response = $response->withCache(filemtime($assetFile), $this->_cacheTime) + ->withFile($assetFile); + + return $response; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Filter/ControllerFactoryFilter.php b/app/vendor/cakephp/cakephp/src/Routing/Filter/ControllerFactoryFilter.php new file mode 100644 index 000000000..7a6164c59 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Filter/ControllerFactoryFilter.php @@ -0,0 +1,66 @@ +getData('request'); + $response = $event->getData('response'); + $event->setData('controller', $this->_getController($request, $response)); + } + + /** + * Gets controller to use, either plugin or application controller. + * + * @param \Cake\Http\ServerRequest $request Request object + * @param \Cake\Http\Response $response Response for the controller. + * @return \Cake\Controller\Controller + * @throws \ReflectionException + */ + protected function _getController($request, $response) + { + $factory = new ControllerFactory(); + + return $factory->create($request, $response); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Filter/LocaleSelectorFilter.php b/app/vendor/cakephp/cakephp/src/Routing/Filter/LocaleSelectorFilter.php new file mode 100644 index 000000000..63a9ada1f --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Filter/LocaleSelectorFilter.php @@ -0,0 +1,71 @@ +_locales = $config['locales']; + } + } + + /** + * Inspects the request for the Accept-Language header and sets the + * Locale for the current runtime if it matches the list of valid locales + * as passed in the configuration. + * + * @param \Cake\Event\Event $event The event instance. + * @return void + */ + public function beforeDispatch(Event $event) + { + /* @var \Cake\Http\ServerRequest $request */ + $request = $event->getData('request'); + $locale = Locale::acceptFromHttp($request->getHeaderLine('Accept-Language')); + + if (!$locale || (!empty($this->_locales) && !in_array($locale, $this->_locales))) { + return; + } + + I18n::setLocale($locale); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Filter/RoutingFilter.php b/app/vendor/cakephp/cakephp/src/Routing/Filter/RoutingFilter.php new file mode 100644 index 000000000..ed9f378a7 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Filter/RoutingFilter.php @@ -0,0 +1,73 @@ +getData('request'); + if (Router::getRequest(true) !== $request) { + Router::setRequestInfo($request); + } + + try { + if (!$request->getParam('controller')) { + $params = Router::parseRequest($request); + $request->addParams($params); + } + + return null; + } catch (RedirectException $e) { + $event->stopPropagation(); + /* @var \Cake\Http\Response $response */ + $response = $event->getData('response'); + $response = $response->withStatus($e->getCode()) + ->withLocation($e->getMessage()); + + return $response; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Middleware/AssetMiddleware.php b/app/vendor/cakephp/cakephp/src/Routing/Middleware/AssetMiddleware.php new file mode 100644 index 000000000..50f83960a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Middleware/AssetMiddleware.php @@ -0,0 +1,197 @@ + 'text/css', + 'json' => 'application/json', + 'js' => 'application/javascript', + 'ico' => 'image/x-icon', + 'eot' => 'application/vnd.ms-fontobject', + 'svg' => 'image/svg+xml', + 'html' => 'text/html', + 'rss' => 'application/rss+xml', + 'xml' => 'application/xml', + ]; + + /** + * Constructor. + * + * @param array $options The options to use + */ + public function __construct(array $options = []) + { + if (!empty($options['cacheTime'])) { + $this->cacheTime = $options['cacheTime']; + } + if (!empty($options['types'])) { + $this->typeMap = array_merge($this->typeMap, $options['types']); + } + } + + /** + * Serve assets if the path matches one. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param \Psr\Http\Message\ResponseInterface $response The response. + * @param callable $next Callback to invoke the next middleware. + * @return \Psr\Http\Message\ResponseInterface A response + */ + public function __invoke($request, $response, $next) + { + $url = $request->getUri()->getPath(); + if (strpos($url, '..') !== false || strpos($url, '.') === false) { + return $next($request, $response); + } + + if (strpos($url, '/.') !== false) { + return $next($request, $response); + } + + $assetFile = $this->_getAssetFile($url); + if ($assetFile === null || !file_exists($assetFile)) { + return $next($request, $response); + } + + $file = new File($assetFile); + $modifiedTime = $file->lastChange(); + if ($this->isNotModified($request, $file)) { + $headers = $response->getHeaders(); + $headers['Last-Modified'] = date(DATE_RFC850, $modifiedTime); + + return new Response('php://memory', 304, $headers); + } + + return $this->deliverAsset($request, $response, $file); + } + + /** + * Check the not modified header. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request to check. + * @param \Cake\Filesystem\File $file The file object to compare. + * @return bool + */ + protected function isNotModified($request, $file) + { + $modifiedSince = $request->getHeaderLine('If-Modified-Since'); + if (!$modifiedSince) { + return false; + } + + return strtotime($modifiedSince) === $file->lastChange(); + } + + /** + * Builds asset file path based off url + * + * @param string $url Asset URL + * @return string Absolute path for asset file + */ + protected function _getAssetFile($url) + { + $parts = explode('/', ltrim($url, '/')); + $pluginPart = []; + for ($i = 0; $i < 2; $i++) { + if (!isset($parts[$i])) { + break; + } + $pluginPart[] = Inflector::camelize($parts[$i]); + $plugin = implode('/', $pluginPart); + if ($plugin && Plugin::loaded($plugin)) { + $parts = array_slice($parts, $i + 1); + $fileFragment = implode(DIRECTORY_SEPARATOR, $parts); + $pluginWebroot = Plugin::path($plugin) . 'webroot' . DIRECTORY_SEPARATOR; + + return $pluginWebroot . $fileFragment; + } + } + + return ''; + } + + /** + * Sends an asset file to the client + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request object to use. + * @param \Psr\Http\Message\ResponseInterface $response The response object to use. + * @param \Cake\Filesystem\File $file The file wrapper for the file. + * @return \Psr\Http\Message\ResponseInterface The response with the file & headers. + */ + protected function deliverAsset(ServerRequestInterface $request, ResponseInterface $response, $file) + { + $contentType = $this->getType($file); + $modified = $file->lastChange(); + $expire = strtotime($this->cacheTime); + $maxAge = $expire - time(); + + $stream = new Stream(fopen($file->path, 'rb')); + + return $response->withBody($stream) + ->withHeader('Content-Type', $contentType) + ->withHeader('Cache-Control', 'public,max-age=' . $maxAge) + ->withHeader('Date', gmdate('D, j M Y G:i:s \G\M\T', time())) + ->withHeader('Last-Modified', gmdate('D, j M Y G:i:s \G\M\T', $modified)) + ->withHeader('Expires', gmdate('D, j M Y G:i:s \G\M\T', $expire)); + } + + /** + * Return the type from a File object + * + * @param File $file The file from which you get the type + * @return string + */ + protected function getType($file) + { + $extension = $file->ext(); + if (isset($this->typeMap[$extension])) { + return $this->typeMap[$extension]; + } + + return $file->mime() ?: 'application/octet-stream'; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Middleware/RoutingMiddleware.php b/app/vendor/cakephp/cakephp/src/Routing/Middleware/RoutingMiddleware.php new file mode 100644 index 000000000..9bb0eb852 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Middleware/RoutingMiddleware.php @@ -0,0 +1,165 @@ +app = $app; + $this->cacheConfig = $cacheConfig; + } + + /** + * Trigger the application's routes() hook if the application exists and Router isn't initialized. + * Uses the routes cache if enabled via configuration param "Router.cache" + * + * If the middleware is created without an Application, routes will be + * loaded via the automatic route loading that pre-dates the routes() hook. + * + * @return void + */ + protected function loadRoutes() + { + if (!$this->app) { + return; + } + + $routeCollection = $this->buildRouteCollection(); + Router::setRouteCollection($routeCollection); + } + + /** + * Check if route cache is enabled and use the configured Cache to 'remember' the route collection + * + * @return \Cake\Routing\RouteCollection + */ + protected function buildRouteCollection() + { + if (Cache::enabled() && $this->cacheConfig !== null) { + return Cache::remember(static::ROUTE_COLLECTION_CACHE_KEY, function () { + return $this->prepareRouteCollection(); + }, $this->cacheConfig); + } + + return $this->prepareRouteCollection(); + } + + /** + * Generate the route collection using the builder + * + * @return \Cake\Routing\RouteCollection + */ + protected function prepareRouteCollection() + { + $builder = Router::createRouteBuilder('/'); + $this->app->routes($builder); + if ($this->app instanceof PluginApplicationInterface) { + $this->app->pluginRoutes($builder); + } + + return Router::getRouteCollection(); + } + + /** + * Apply routing and update the request. + * + * Any route/path specific middleware will be wrapped around $next and then the new middleware stack will be + * invoked. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param \Psr\Http\Message\ResponseInterface $response The response. + * @param callable $next The next middleware to call. + * @return \Psr\Http\Message\ResponseInterface A response. + * @throws \Cake\Routing\InvalidArgumentException + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) + { + $this->loadRoutes(); + try { + Router::setRequestContext($request); + $params = (array)$request->getAttribute('params', []); + $middleware = []; + if (empty($params['controller'])) { + $parsedBody = $request->getParsedBody(); + if (is_array($parsedBody) && isset($parsedBody['_method'])) { + $request = $request->withMethod($parsedBody['_method']); + } + $params = Router::parseRequest($request) + $params; + if (isset($params['_middleware'])) { + $middleware = $params['_middleware']; + unset($params['_middleware']); + } + $request = $request->withAttribute('params', $params); + } + } catch (RedirectException $e) { + return new RedirectResponse( + $e->getMessage(), + $e->getCode(), + $response->getHeaders() + ); + } + $matching = Router::getRouteCollection()->getMiddleware($middleware); + if (!$matching) { + return $next($request, $response); + } + $matching[] = $next; + $middleware = new MiddlewareQueue($matching); + $runner = new Runner(); + + return $runner->run($middleware, $request, $response); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/RequestActionTrait.php b/app/vendor/cakephp/cakephp/src/Routing/RequestActionTrait.php new file mode 100644 index 000000000..1cc2c2f1a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/RequestActionTrait.php @@ -0,0 +1,186 @@ +requestAction('/articles/popular'); + * ``` + * + * A basic example of request action to fetch a rendered page without the layout. + * + * ``` + * $viewHtml = $this->requestAction('/articles/popular', ['return']); + * ``` + * + * You can also pass the URL as an array: + * + * ``` + * $vars = $this->requestAction(['controller' => 'articles', 'action' => 'popular']); + * ``` + * + * ### Passing other request data + * + * You can pass POST, GET, COOKIE and other data into the request using the appropriate keys. + * Cookies can be passed using the `cookies` key. Get parameters can be set with `query` and post + * data can be sent using the `post` key. + * + * ``` + * $vars = $this->requestAction('/articles/popular', [ + * 'query' => ['page' => 1], + * 'cookies' => ['remember_me' => 1], + * ]); + * ``` + * + * ### Sending environment or header values + * + * By default actions dispatched with this method will use the global $_SERVER and $_ENV + * values. If you want to override those values for a request action, you can specify the values: + * + * ``` + * $vars = $this->requestAction('/articles/popular', [ + * 'environment' => ['CONTENT_TYPE' => 'application/json'] + * ]); + * ``` + * + * ### Transmitting the session + * + * By default actions dispatched with this method will use the standard session object. + * If you want a particular session instance to be used, you need to specify it. + * + * ``` + * $vars = $this->requestAction('/articles/popular', [ + * 'session' => new Session($someSessionConfig) + * ]); + * ``` + * + * @param string|array $url String or array-based url. Unlike other url arrays in CakePHP, this + * url will not automatically handle passed arguments in the $url parameter. + * @param array $extra if array includes the key "return" it sets the autoRender to true. Can + * also be used to submit GET/POST data, and passed arguments. + * @return mixed Boolean true or false on success/failure, or contents + * of rendered action if 'return' is set in $extra. + * @deprecated 3.3.0 You should refactor your code to use View Cells instead of this method. + */ + public function requestAction($url, array $extra = []) + { + deprecationWarning( + 'RequestActionTrait::requestAction() is deprecated. ' . + 'You should refactor to use View Cells or Components instead.' + ); + if (empty($url)) { + return false; + } + if (($index = array_search('return', $extra)) !== false) { + $extra['return'] = 0; + $extra['autoRender'] = 1; + unset($extra[$index]); + } + $extra += ['autoRender' => 0, 'return' => 1, 'bare' => 1, 'requested' => 1]; + + $baseUrl = Configure::read('App.fullBaseUrl'); + if (is_string($url) && strpos($url, $baseUrl) === 0) { + $url = Router::normalize(str_replace($baseUrl, '', $url)); + } + if (is_string($url)) { + $params = [ + 'url' => $url + ]; + } elseif (is_array($url)) { + $defaultParams = ['plugin' => null, 'controller' => null, 'action' => null]; + $params = [ + 'params' => $url + $defaultParams, + 'base' => false, + 'url' => Router::reverse($url) + ]; + if (empty($params['params']['pass'])) { + $params['params']['pass'] = []; + } + } + $current = Router::getRequest(); + if ($current) { + $params['base'] = $current->getAttribute('base'); + $params['webroot'] = $current->getAttribute('webroot'); + } + + $params['post'] = $params['query'] = []; + if (isset($extra['post'])) { + $params['post'] = $extra['post']; + } + if (isset($extra['query'])) { + $params['query'] = $extra['query']; + } + if (isset($extra['cookies'])) { + $params['cookies'] = $extra['cookies']; + } + if (isset($extra['environment'])) { + $params['environment'] = $extra['environment'] + $_SERVER + $_ENV; + } + unset($extra['environment'], $extra['post'], $extra['query']); + + $params['session'] = isset($extra['session']) ? $extra['session'] : new Session(); + + $request = new ServerRequest($params); + $request->addParams($extra); + $dispatcher = DispatcherFactory::create(); + + // If an application is using PSR7 middleware, + // we need to 'fix' their missing dispatcher filters. + $needed = [ + 'routing' => RoutingFilter::class, + 'controller' => ControllerFactoryFilter::class + ]; + foreach ($dispatcher->filters() as $filter) { + if ($filter instanceof RoutingFilter) { + unset($needed['routing']); + } + if ($filter instanceof ControllerFactoryFilter) { + unset($needed['controller']); + } + } + foreach ($needed as $class) { + $dispatcher->addFilter(new $class); + } + $result = $dispatcher->dispatch($request, new Response()); + Router::popRequest(); + + return $result; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/DashedRoute.php b/app/vendor/cakephp/cakephp/src/Routing/Route/DashedRoute.php new file mode 100644 index 000000000..71ff7f436 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Route/DashedRoute.php @@ -0,0 +1,123 @@ + 'MyPlugin', 'controller' => 'MyController', 'action' => 'myAction']` + */ +class DashedRoute extends Route +{ + + /** + * Flag for tracking whether or not the defaults have been inflected. + * + * Default values need to be inflected so that they match the inflections that + * match() will create. + * + * @var bool + */ + protected $_inflectedDefaults = false; + + /** + * Camelizes the previously dashed plugin route taking into account plugin vendors + * + * @param string $plugin Plugin name + * @return string + */ + protected function _camelizePlugin($plugin) + { + $plugin = str_replace('-', '_', $plugin); + if (strpos($plugin, '/') === false) { + return Inflector::camelize($plugin); + } + list($vendor, $plugin) = explode('/', $plugin, 2); + + return Inflector::camelize($vendor) . '/' . Inflector::camelize($plugin); + } + + /** + * Parses a string URL into an array. If it matches, it will convert the + * controller and plugin keys to their CamelCased form and action key to + * camelBacked form. + * + * @param string $url The URL to parse + * @param string $method The HTTP method. + * @return array|false An array of request parameters, or false on failure. + */ + public function parse($url, $method = '') + { + $params = parent::parse($url, $method); + if (!$params) { + return false; + } + if (!empty($params['controller'])) { + $params['controller'] = Inflector::camelize($params['controller'], '-'); + } + if (!empty($params['plugin'])) { + $params['plugin'] = $this->_camelizePlugin($params['plugin']); + } + if (!empty($params['action'])) { + $params['action'] = Inflector::variable(str_replace( + '-', + '_', + $params['action'] + )); + } + + return $params; + } + + /** + * Dasherizes the controller, action and plugin params before passing them on + * to the parent class. + * + * @param array $url Array of parameters to convert to a string. + * @param array $context An array of the current request context. + * Contains information such as the current host, scheme, port, and base + * directory. + * @return bool|string Either false or a string URL. + */ + public function match(array $url, array $context = []) + { + $url = $this->_dasherize($url); + if (!$this->_inflectedDefaults) { + $this->_inflectedDefaults = true; + $this->defaults = $this->_dasherize($this->defaults); + } + + return parent::match($url, $context); + } + + /** + * Helper method for dasherizing keys in a URL array. + * + * @param array $url An array of URL keys. + * @return array + */ + protected function _dasherize($url) + { + foreach (['controller', 'plugin', 'action'] as $element) { + if (!empty($url[$element])) { + $url[$element] = Inflector::dasherize($url[$element]); + } + } + + return $url; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/EntityRoute.php b/app/vendor/cakephp/cakephp/src/Routing/Route/EntityRoute.php new file mode 100644 index 000000000..97ac74f63 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Route/EntityRoute.php @@ -0,0 +1,77 @@ +_checkEntity($entity); + + preg_match_all('@:(\w+)@', $this->template, $matches); + + foreach ($matches[1] as $field) { + if (!isset($url[$field]) && isset($entity[$field])) { + $url[$field] = $entity[$field]; + } + } + } + + return parent::match($url, $context); + } + + /** + * Checks that we really deal with an entity object + * + * @throws \RuntimeException + * @param \ArrayAccess|array $entity Entity value from the URL options + * @return void + */ + protected function _checkEntity($entity) + { + if (!$entity instanceof ArrayAccess && !is_array($entity)) { + throw new RuntimeException(sprintf( + 'Route `%s` expects the URL option `_entity` to be an array or object implementing \ArrayAccess, but `%s` passed.', + $this->template, + getTypeName($entity) + )); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/InflectedRoute.php b/app/vendor/cakephp/cakephp/src/Routing/Route/InflectedRoute.php new file mode 100644 index 000000000..face5d188 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Route/InflectedRoute.php @@ -0,0 +1,103 @@ + 'MyController']` + */ +class InflectedRoute extends Route +{ + + /** + * Flag for tracking whether or not the defaults have been inflected. + * + * Default values need to be inflected so that they match the inflections that match() + * will create. + * + * @var bool + */ + protected $_inflectedDefaults = false; + + /** + * Parses a string URL into an array. If it matches, it will convert the prefix, controller and + * plugin keys to their camelized form. + * + * @param string $url The URL to parse + * @param string $method The HTTP method being matched. + * @return array|false An array of request parameters, or false on failure. + */ + public function parse($url, $method = '') + { + $params = parent::parse($url, $method); + if (!$params) { + return false; + } + if (!empty($params['controller'])) { + $params['controller'] = Inflector::camelize($params['controller']); + } + if (!empty($params['plugin'])) { + if (strpos($params['plugin'], '/') === false) { + $params['plugin'] = Inflector::camelize($params['plugin']); + } else { + list($vendor, $plugin) = explode('/', $params['plugin'], 2); + $params['plugin'] = Inflector::camelize($vendor) . '/' . Inflector::camelize($plugin); + } + } + + return $params; + } + + /** + * Underscores the prefix, controller and plugin params before passing them on to the + * parent class + * + * @param array $url Array of parameters to convert to a string. + * @param array $context An array of the current request context. + * Contains information such as the current host, scheme, port, and base + * directory. + * @return string|false Either a string URL for the parameters if they match or false. + */ + public function match(array $url, array $context = []) + { + $url = $this->_underscore($url); + if (!$this->_inflectedDefaults) { + $this->_inflectedDefaults = true; + $this->defaults = $this->_underscore($this->defaults); + } + + return parent::match($url, $context); + } + + /** + * Helper method for underscoring keys in a URL array. + * + * @param array $url An array of URL keys. + * @return array + */ + protected function _underscore($url) + { + if (!empty($url['controller'])) { + $url['controller'] = Inflector::underscore($url['controller']); + } + if (!empty($url['plugin'])) { + $url['plugin'] = Inflector::underscore($url['plugin']); + } + + return $url; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/PluginShortRoute.php b/app/vendor/cakephp/cakephp/src/Routing/Route/PluginShortRoute.php new file mode 100644 index 000000000..2e7cf7d50 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Route/PluginShortRoute.php @@ -0,0 +1,64 @@ +defaults['controller'] = $url['controller']; + $result = parent::match($url, $context); + unset($this->defaults['controller']); + + return $result; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/RedirectRoute.php b/app/vendor/cakephp/cakephp/src/Routing/Route/RedirectRoute.php new file mode 100644 index 000000000..e4ccc3d16 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Route/RedirectRoute.php @@ -0,0 +1,111 @@ +redirect = (array)$defaults; + } + + /** + * Parses a string URL into an array. Parsed URLs will result in an automatic + * redirection. + * + * @param string $url The URL to parse. + * @param string $method The HTTP method being used. + * @return bool|null False on failure. An exception is raised on a successful match. + * @throws \Cake\Routing\Exception\RedirectException An exception is raised on successful match. + * This is used to halt route matching and signal to the middleware that a redirect should happen. + */ + public function parse($url, $method = '') + { + $params = parent::parse($url, $method); + if (!$params) { + return false; + } + $redirect = $this->redirect; + if (count($this->redirect) === 1 && !isset($this->redirect['controller'])) { + $redirect = $this->redirect[0]; + } + if (isset($this->options['persist']) && is_array($redirect)) { + $redirect += ['pass' => $params['pass'], 'url' => []]; + if (is_array($this->options['persist'])) { + foreach ($this->options['persist'] as $elem) { + if (isset($params[$elem])) { + $redirect[$elem] = $params[$elem]; + } + } + } + $redirect = Router::reverseToArray($redirect); + } + $status = 301; + if (isset($this->options['status']) && ($this->options['status'] >= 300 && $this->options['status'] < 400)) { + $status = $this->options['status']; + } + throw new RedirectException(Router::url($redirect, true), $status); + } + + /** + * There is no reverse routing redirection routes. + * + * @param array $url Array of parameters to convert to a string. + * @param array $context Array of request context parameters. + * @return bool Always false. + */ + public function match(array $url, array $context = []) + { + return false; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/Route.php b/app/vendor/cakephp/cakephp/src/Routing/Route/Route.php new file mode 100644 index 000000000..77a59d263 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Route/Route.php @@ -0,0 +1,926 @@ +template = $template; + if (isset($defaults['[method]'])) { + deprecationWarning('The `[method]` option is deprecated. Use `_method` instead.'); + $defaults['_method'] = $defaults['[method]']; + unset($defaults['[method]']); + } + $this->defaults = (array)$defaults; + $this->options = $options + ['_ext' => [], '_middleware' => []]; + $this->setExtensions((array)$this->options['_ext']); + $this->setMiddleware((array)$this->options['_middleware']); + unset($this->options['_middleware']); + } + + /** + * Get/Set the supported extensions for this route. + * + * @deprecated 3.3.9 Use getExtensions/setExtensions instead. + * @param null|string|array $extensions The extensions to set. Use null to get. + * @return array|null The extensions or null. + */ + public function extensions($extensions = null) + { + deprecationWarning( + 'Route::extensions() is deprecated. ' . + 'Use Route::setExtensions()/getExtensions() instead.' + ); + if ($extensions === null) { + return $this->_extensions; + } + $this->_extensions = (array)$extensions; + } + + /** + * Set the supported extensions for this route. + * + * @param array $extensions The extensions to set. + * @return $this + */ + public function setExtensions(array $extensions) + { + $this->_extensions = []; + foreach ($extensions as $ext) { + $this->_extensions[] = strtolower($ext); + } + + return $this; + } + + /** + * Get the supported extensions for this route. + * + * @return array + */ + public function getExtensions() + { + return $this->_extensions; + } + + /** + * Set the accepted HTTP methods for this route. + * + * @param array $methods The HTTP methods to accept. + * @return $this + * @throws \InvalidArgumentException + */ + public function setMethods(array $methods) + { + $methods = array_map('strtoupper', $methods); + $diff = array_diff($methods, static::VALID_METHODS); + if ($diff !== []) { + throw new InvalidArgumentException( + sprintf('Invalid HTTP method received. %s is invalid.', implode(', ', $diff)) + ); + } + $this->defaults['_method'] = $methods; + + return $this; + } + + /** + * Set regexp patterns for routing parameters + * + * If any of your patterns contain multibyte values, the `multibytePattern` + * mode will be enabled. + * + * @param array $patterns The patterns to apply to routing elements + * @return $this + */ + public function setPatterns(array $patterns) + { + $patternValues = implode("", $patterns); + if (mb_strlen($patternValues) < strlen($patternValues)) { + $this->options['multibytePattern'] = true; + } + $this->options = array_merge($this->options, $patterns); + + return $this; + } + + /** + * Set host requirement + * + * @param string $host The host name this route is bound to + * @return $this + */ + public function setHost($host) + { + $this->options['_host'] = $host; + + return $this; + } + + /** + * Set the names of parameters that will be converted into passed parameters + * + * @param array $names The names of the parameters that should be passed. + * @return $this + */ + public function setPass(array $names) + { + $this->options['pass'] = $names; + + return $this; + } + + /** + * Set the names of parameters that will persisted automatically + * + * Persistent parametesr allow you to define which route parameters should be automatically + * included when generating new URLs. You can override persistent parameters + * by redefining them in a URL or remove them by setting the persistent parameter to `false`. + * + * ``` + * // remove a persistent 'date' parameter + * Router::url(['date' => false', ...]); + * ``` + * + * @param array $names The names of the parameters that should be passed. + * @return $this + */ + public function setPersist(array $names) + { + $this->options['persist'] = $names; + + return $this; + } + + /** + * Check if a Route has been compiled into a regular expression. + * + * @return bool + */ + public function compiled() + { + return !empty($this->_compiledRoute); + } + + /** + * Compiles the route's regular expression. + * + * Modifies defaults property so all necessary keys are set + * and populates $this->names with the named routing elements. + * + * @return string Returns a string regular expression of the compiled route. + */ + public function compile() + { + if ($this->_compiledRoute) { + return $this->_compiledRoute; + } + $this->_writeRoute(); + + return $this->_compiledRoute; + } + + /** + * Builds a route regular expression. + * + * Uses the template, defaults and options properties to compile a + * regular expression that can be used to parse request strings. + * + * @return void + */ + protected function _writeRoute() + { + if (empty($this->template) || ($this->template === '/')) { + $this->_compiledRoute = '#^/*$#'; + $this->keys = []; + + return; + } + $route = $this->template; + $names = $routeParams = []; + $parsed = preg_quote($this->template, '#'); + + if (strpos($route, '{') !== false && strpos($route, '}') !== false) { + preg_match_all('/\{([a-z][a-z0-9-_]*)\}/i', $route, $namedElements); + $this->braceKeys = true; + } else { + preg_match_all('/:([a-z0-9-_]+(?braceKeys = false; + } + foreach ($namedElements[1] as $i => $name) { + $search = preg_quote($namedElements[0][$i]); + if (isset($this->options[$name])) { + $option = null; + if ($name !== 'plugin' && array_key_exists($name, $this->defaults)) { + $option = '?'; + } + $slashParam = '/' . $search; + if (strpos($parsed, $slashParam) !== false) { + $routeParams[$slashParam] = '(?:/(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option; + } else { + $routeParams[$search] = '(?:(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option; + } + } else { + $routeParams[$search] = '(?:(?P<' . $name . '>[^/]+))'; + } + $names[] = $name; + } + if (preg_match('#\/\*\*$#', $route)) { + $parsed = preg_replace('#/\\\\\*\\\\\*$#', '(?:/(?P<_trailing_>.*))?', $parsed); + $this->_greedy = true; + } + if (preg_match('#\/\*$#', $route)) { + $parsed = preg_replace('#/\\\\\*$#', '(?:/(?P<_args_>.*))?', $parsed); + $this->_greedy = true; + } + $mode = ''; + if (!empty($this->options['multibytePattern'])) { + $mode = 'u'; + } + krsort($routeParams); + $parsed = str_replace(array_keys($routeParams), $routeParams, $parsed); + $this->_compiledRoute = '#^' . $parsed . '[/]*$#' . $mode; + $this->keys = $names; + + // Remove defaults that are also keys. They can cause match failures + foreach ($this->keys as $key) { + unset($this->defaults[$key]); + } + + $keys = $this->keys; + sort($keys); + $this->keys = array_reverse($keys); + } + + /** + * Get the standardized plugin.controller:action name for a route. + * + * @return string + */ + public function getName() + { + if (!empty($this->_name)) { + return $this->_name; + } + $name = ''; + $keys = [ + 'prefix' => ':', + 'plugin' => '.', + 'controller' => ':', + 'action' => '' + ]; + foreach ($keys as $key => $glue) { + $value = null; + if (strpos($this->template, ':' . $key) !== false) { + $value = '_' . $key; + } elseif (isset($this->defaults[$key])) { + $value = $this->defaults[$key]; + } + + if ($value === null) { + continue; + } + if ($value === true || $value === false) { + $value = $value ? '1' : '0'; + } + $name .= $value . $glue; + } + + return $this->_name = strtolower($name); + } + + /** + * Checks to see if the given URL can be parsed by this route. + * + * If the route can be parsed an array of parameters will be returned; if not + * false will be returned. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The URL to attempt to parse. + * @return array|false An array of request parameters, or false on failure. + */ + public function parseRequest(ServerRequestInterface $request) + { + $uri = $request->getUri(); + if (isset($this->options['_host']) && !$this->hostMatches($uri->getHost())) { + return false; + } + + return $this->parse($uri->getPath(), $request->getMethod()); + } + + /** + * Checks to see if the given URL can be parsed by this route. + * + * If the route can be parsed an array of parameters will be returned; if not + * false will be returned. String URLs are parsed if they match a routes regular expression. + * + * @param string $url The URL to attempt to parse. + * @param string $method The HTTP method of the request being parsed. + * @return array|false An array of request parameters, or false on failure. + * @deprecated 3.4.0 Use/implement parseRequest() instead as it provides more flexibility/control. + */ + public function parse($url, $method = '') + { + if (empty($this->_compiledRoute)) { + $this->compile(); + } + list($url, $ext) = $this->_parseExtension($url); + + if (!preg_match($this->_compiledRoute, urldecode($url), $route)) { + return false; + } + + if (isset($this->defaults['_method'])) { + if (empty($method)) { + deprecationWarning( + 'Extracting the request method from global state when parsing routes is deprecated. ' . + 'Instead adopt Route::parseRequest() which extracts the method from the passed request.' + ); + // Deprecated reading the global state is deprecated and will be removed in 4.x + $request = Router::getRequest(true) ?: ServerRequestFactory::fromGlobals(); + $method = $request->getMethod(); + } + if (!in_array($method, (array)$this->defaults['_method'], true)) { + return false; + } + } + + array_shift($route); + $count = count($this->keys); + for ($i = 0; $i <= $count; $i++) { + unset($route[$i]); + } + $route['pass'] = []; + + // Assign defaults, set passed args to pass + foreach ($this->defaults as $key => $value) { + if (isset($route[$key])) { + continue; + } + if (is_int($key)) { + $route['pass'][] = $value; + continue; + } + $route[$key] = $value; + } + + if (isset($route['_args_'])) { + $pass = $this->_parseArgs($route['_args_'], $route); + $route['pass'] = array_merge($route['pass'], $pass); + unset($route['_args_']); + } + + if (isset($route['_trailing_'])) { + $route['pass'][] = $route['_trailing_']; + unset($route['_trailing_']); + } + + if (!empty($ext)) { + $route['_ext'] = $ext; + } + + // pass the name if set + if (isset($this->options['_name'])) { + $route['_name'] = $this->options['_name']; + } + + // restructure 'pass' key route params + if (isset($this->options['pass'])) { + $j = count($this->options['pass']); + while ($j--) { + if (isset($route[$this->options['pass'][$j]])) { + array_unshift($route['pass'], $route[$this->options['pass'][$j]]); + } + } + } + $route['_matchedRoute'] = $this->template; + if (count($this->middleware) > 0) { + $route['_middleware'] = $this->middleware; + } + + return $route; + } + + /** + * Check to see if the host matches the route requirements + * + * @param string $host The request's host name + * @return bool Whether or not the host matches any conditions set in for this route. + */ + public function hostMatches($host) + { + $pattern = '@^' . str_replace('\*', '.*', preg_quote($this->options['_host'], '@')) . '$@'; + + return preg_match($pattern, $host) !== 0; + } + + /** + * Removes the extension from $url if it contains a registered extension. + * If no registered extension is found, no extension is returned and the URL is returned unmodified. + * + * @param string $url The url to parse. + * @return array containing url, extension + */ + protected function _parseExtension($url) + { + if (count($this->_extensions) && strpos($url, '.') !== false) { + foreach ($this->_extensions as $ext) { + $len = strlen($ext) + 1; + if (substr($url, -$len) === '.' . $ext) { + return [substr($url, 0, $len * -1), $ext]; + } + } + } + + return [$url, null]; + } + + /** + * Parse passed parameters into a list of passed args. + * + * Return true if a given named $param's $val matches a given $rule depending on $context. + * Currently implemented rule types are controller, action and match that can be combined with each other. + * + * @param string $args A string with the passed params. eg. /1/foo + * @param string $context The current route context, which should contain controller/action keys. + * @return array Array of passed args. + */ + protected function _parseArgs($args, $context) + { + $pass = []; + $args = explode('/', $args); + + foreach ($args as $param) { + if (empty($param) && $param !== '0' && $param !== 0) { + continue; + } + $pass[] = rawurldecode($param); + } + + return $pass; + } + + /** + * Apply persistent parameters to a URL array. Persistent parameters are a + * special key used during route creation to force route parameters to + * persist when omitted from a URL array. + * + * @param array $url The array to apply persistent parameters to. + * @param array $params An array of persistent values to replace persistent ones. + * @return array An array with persistent parameters applied. + */ + protected function _persistParams(array $url, array $params) + { + foreach ($this->options['persist'] as $persistKey) { + if (array_key_exists($persistKey, $params) && !isset($url[$persistKey])) { + $url[$persistKey] = $params[$persistKey]; + } + } + + return $url; + } + + /** + * Check if a URL array matches this route instance. + * + * If the URL matches the route parameters and settings, then + * return a generated string URL. If the URL doesn't match the route parameters, false will be returned. + * This method handles the reverse routing or conversion of URL arrays into string URLs. + * + * @param array $url An array of parameters to check matching with. + * @param array $context An array of the current request context. + * Contains information such as the current host, scheme, port, base + * directory and other url params. + * @return string|false Either a string URL for the parameters if they match or false. + */ + public function match(array $url, array $context = []) + { + if (empty($this->_compiledRoute)) { + $this->compile(); + } + $defaults = $this->defaults; + $context += ['params' => [], '_port' => null, '_scheme' => null, '_host' => null]; + + if (!empty($this->options['persist']) && + is_array($this->options['persist']) + ) { + $url = $this->_persistParams($url, $context['params']); + } + unset($context['params']); + $hostOptions = array_intersect_key($url, $context); + + // Apply the _host option if possible + if (isset($this->options['_host'])) { + if (!isset($hostOptions['_host']) && strpos($this->options['_host'], '*') === false) { + $hostOptions['_host'] = $this->options['_host']; + } + if (!isset($hostOptions['_host'])) { + $hostOptions['_host'] = $context['_host']; + } + + // The host did not match the route preferences + if (!$this->hostMatches($hostOptions['_host'])) { + return false; + } + } + + // Check for properties that will cause an + // absolute url. Copy the other properties over. + if (isset($hostOptions['_scheme']) || + isset($hostOptions['_port']) || + isset($hostOptions['_host']) + ) { + $hostOptions += $context; + + if (getservbyname($hostOptions['_scheme'], 'tcp') === $hostOptions['_port']) { + unset($hostOptions['_port']); + } + } + + // If no base is set, copy one in. + if (!isset($hostOptions['_base']) && isset($context['_base'])) { + $hostOptions['_base'] = $context['_base']; + } + + $query = !empty($url['?']) ? (array)$url['?'] : []; + unset($url['_host'], $url['_scheme'], $url['_port'], $url['_base'], $url['?']); + + // Move extension into the hostOptions so its not part of + // reverse matches. + if (isset($url['_ext'])) { + $hostOptions['_ext'] = $url['_ext']; + unset($url['_ext']); + } + + // Check the method first as it is special. + if (!$this->_matchMethod($url)) { + return false; + } + unset($url['_method'], $url['[method]'], $defaults['_method']); + + // Missing defaults is a fail. + if (array_diff_key($defaults, $url) !== []) { + return false; + } + + // Defaults with different values are a fail. + if (array_intersect_key($url, $defaults) != $defaults) { + return false; + } + + // If this route uses pass option, and the passed elements are + // not set, rekey elements. + if (isset($this->options['pass'])) { + foreach ($this->options['pass'] as $i => $name) { + if (isset($url[$i]) && !isset($url[$name])) { + $url[$name] = $url[$i]; + unset($url[$i]); + } + } + } + + // check that all the key names are in the url + $keyNames = array_flip($this->keys); + if (array_intersect_key($keyNames, $url) !== $keyNames) { + return false; + } + + $pass = []; + foreach ($url as $key => $value) { + // keys that exist in the defaults and have different values is a match failure. + $defaultExists = array_key_exists($key, $defaults); + + // If the key is a routed key, it's not different yet. + if (array_key_exists($key, $keyNames)) { + continue; + } + + // pull out passed args + $numeric = is_numeric($key); + if ($numeric && isset($defaults[$key]) && $defaults[$key] == $value) { + continue; + } + if ($numeric) { + $pass[] = $value; + unset($url[$key]); + continue; + } + + // keys that don't exist are different. + if (!$defaultExists && ($value !== null && $value !== false && $value !== '')) { + $query[$key] = $value; + unset($url[$key]); + } + } + + // if not a greedy route, no extra params are allowed. + if (!$this->_greedy && !empty($pass)) { + return false; + } + + // check patterns for routed params + if (!empty($this->options)) { + foreach ($this->options as $key => $pattern) { + if (isset($url[$key]) && !preg_match('#^' . $pattern . '$#u', $url[$key])) { + return false; + } + } + } + $url += $hostOptions; + + return $this->_writeUrl($url, $pass, $query); + } + + /** + * Check whether or not the URL's HTTP method matches. + * + * @param array $url The array for the URL being generated. + * @return bool + */ + protected function _matchMethod($url) + { + if (empty($this->defaults['_method'])) { + return true; + } + // @deprecated The `[method]` support should be removed in 4.0.0 + if (isset($url['[method]'])) { + deprecationWarning('The `[method]` key is deprecated. Use `_method` instead.'); + $url['_method'] = $url['[method]']; + } + if (empty($url['_method'])) { + return false; + } + $methods = array_map('strtoupper', (array)$url['_method']); + foreach ($methods as $value) { + if (in_array($value, (array)$this->defaults['_method'])) { + return true; + } + } + + return false; + } + + /** + * Converts a matching route array into a URL string. + * + * Composes the string URL using the template + * used to create the route. + * + * @param array $params The params to convert to a string url + * @param array $pass The additional passed arguments + * @param array $query An array of parameters + * @return string Composed route string. + */ + protected function _writeUrl($params, $pass = [], $query = []) + { + $pass = implode('/', array_map('rawurlencode', $pass)); + $out = $this->template; + + $search = $replace = []; + foreach ($this->keys as $key) { + $string = null; + if (isset($params[$key])) { + $string = $params[$key]; + } elseif (strpos($out, $key) != strlen($out) - strlen($key)) { + $key .= '/'; + } + if ($this->braceKeys) { + $search[] = "{{$key}}"; + } else { + $search[] = ':' . $key; + } + $replace[] = $string; + } + + if (strpos($this->template, '**') !== false) { + array_push($search, '**', '%2F'); + array_push($replace, $pass, '/'); + } elseif (strpos($this->template, '*') !== false) { + $search[] = '*'; + $replace[] = $pass; + } + $out = str_replace($search, $replace, $out); + + // add base url if applicable. + if (isset($params['_base'])) { + $out = $params['_base'] . $out; + unset($params['_base']); + } + + $out = str_replace('//', '/', $out); + if (isset($params['_scheme']) || + isset($params['_host']) || + isset($params['_port']) + ) { + $host = $params['_host']; + + // append the port & scheme if they exists. + if (isset($params['_port'])) { + $host .= ':' . $params['_port']; + } + $scheme = isset($params['_scheme']) ? $params['_scheme'] : 'http'; + $out = "{$scheme}://{$host}{$out}"; + } + if (!empty($params['_ext']) || !empty($query)) { + $out = rtrim($out, '/'); + } + if (!empty($params['_ext'])) { + $out .= '.' . $params['_ext']; + } + if (!empty($query)) { + $out .= rtrim('?' . http_build_query($query), '?'); + } + + return $out; + } + + /** + * Get the static path portion for this route. + * + * @return string + */ + public function staticPath() + { + $routeKey = strpos($this->template, ':'); + if ($routeKey !== false) { + return substr($this->template, 0, $routeKey); + } + $routeKey = strpos($this->template, '{'); + if ($routeKey !== false && strpos($this->template, '}') !== false) { + return substr($this->template, 0, $routeKey); + } + $star = strpos($this->template, '*'); + if ($star !== false) { + $path = rtrim(substr($this->template, 0, $star), '/'); + + return $path === '' ? '/' : $path; + } + + return $this->template; + } + + /** + * Set the names of the middleware that should be applied to this route. + * + * @param array $middleware The list of middleware names to apply to this route. + * Middleware names will not be checked until the route is matched. + * @return $this + */ + public function setMiddleware(array $middleware) + { + $this->middleware = $middleware; + + return $this; + } + + /** + * Get the names of the middleware that should be applied to this route. + * + * @return array + */ + public function getMiddleware() + { + return $this->middleware; + } + + /** + * Set state magic method to support var_export + * + * This method helps for applications that want to implement + * router caching. + * + * @param array $fields Key/Value of object attributes + * @return \Cake\Routing\Route\Route A new instance of the route + */ + public static function __set_state($fields) + { + $class = get_called_class(); + $obj = new $class(''); + foreach ($fields as $field => $value) { + $obj->$field = $value; + } + + return $obj; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/RouteBuilder.php b/app/vendor/cakephp/cakephp/src/Routing/RouteBuilder.php new file mode 100644 index 000000000..652a435c0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/RouteBuilder.php @@ -0,0 +1,1083 @@ + controller action map. + * + * @var array + */ + protected static $_resourceMap = [ + 'index' => ['action' => 'index', 'method' => 'GET', 'path' => ''], + 'create' => ['action' => 'add', 'method' => 'POST', 'path' => ''], + 'view' => ['action' => 'view', 'method' => 'GET', 'path' => ':id'], + 'update' => ['action' => 'edit', 'method' => ['PUT', 'PATCH'], 'path' => ':id'], + 'delete' => ['action' => 'delete', 'method' => 'DELETE', 'path' => ':id'], + ]; + + /** + * Default route class to use if none is provided in connect() options. + * + * @var string + */ + protected $_routeClass = 'Cake\Routing\Route\Route'; + + /** + * The extensions that should be set into the routes connected. + * + * @var array + */ + protected $_extensions = []; + + /** + * The path prefix scope that this collection uses. + * + * @var string + */ + protected $_path; + + /** + * The scope parameters if there are any. + * + * @var array + */ + protected $_params; + + /** + * Name prefix for connected routes. + * + * @var string + */ + protected $_namePrefix = ''; + + /** + * The route collection routes should be added to. + * + * @var \Cake\Routing\RouteCollection + */ + protected $_collection; + + /** + * The list of middleware that routes in this builder get + * added during construction. + * + * @var array + */ + protected $middleware = []; + + /** + * Constructor + * + * ### Options + * + * - `routeClass` - The default route class to use when adding routes. + * - `extensions` - The extensions to connect when adding routes. + * - `namePrefix` - The prefix to prepend to all route names. + * - `middleware` - The names of the middleware routes should have applied. + * + * @param \Cake\Routing\RouteCollection $collection The route collection to append routes into. + * @param string $path The path prefix the scope is for. + * @param array $params The scope's routing parameters. + * @param array $options Options list. + */ + public function __construct(RouteCollection $collection, $path, array $params = [], array $options = []) + { + $this->_collection = $collection; + $this->_path = $path; + $this->_params = $params; + if (isset($options['routeClass'])) { + $this->_routeClass = $options['routeClass']; + } + if (isset($options['extensions'])) { + $this->_extensions = $options['extensions']; + } + if (isset($options['namePrefix'])) { + $this->_namePrefix = $options['namePrefix']; + } + if (isset($options['middleware'])) { + $this->middleware = (array)$options['middleware']; + } + } + + /** + * Get or set default route class. + * + * @deprecated 3.5.0 Use getRouteClass/setRouteClass instead. + * @param string|null $routeClass Class name. + * @return string|null + */ + public function routeClass($routeClass = null) + { + deprecationWarning( + 'RouteBuilder::routeClass() is deprecated. ' . + 'Use RouteBuilder::setRouteClass()/getRouteClass() instead.' + ); + if ($routeClass === null) { + return $this->getRouteClass(); + } + $this->setRouteClass($routeClass); + } + + /** + * Set default route class. + * + * @param string $routeClass Class name. + * @return $this + */ + public function setRouteClass($routeClass) + { + $this->_routeClass = $routeClass; + + return $this; + } + + /** + * Get default route class. + * + * @return string + */ + public function getRouteClass() + { + return $this->_routeClass; + } + + /** + * Get or set the extensions in this route builder's scope. + * + * Future routes connected in through this builder will have the connected + * extensions applied. However, setting extensions does not modify existing routes. + * + * @deprecated 3.5.0 Use getExtensions/setExtensions instead. + * @param null|string|array $extensions Either the extensions to use or null. + * @return array|null + */ + public function extensions($extensions = null) + { + deprecationWarning( + 'RouteBuilder::extensions() is deprecated. ' . + 'Use RouteBuilder::setExtensions()/getExtensions() instead.' + ); + if ($extensions === null) { + return $this->getExtensions(); + } + $this->setExtensions($extensions); + } + + /** + * Set the extensions in this route builder's scope. + * + * Future routes connected in through this builder will have the connected + * extensions applied. However, setting extensions does not modify existing routes. + * + * @param string|array $extensions The extensions to set. + * @return $this + */ + public function setExtensions($extensions) + { + $this->_extensions = (array)$extensions; + + return $this; + } + + /** + * Get the extensions in this route builder's scope. + * + * @return array + */ + public function getExtensions() + { + return $this->_extensions; + } + + /** + * Add additional extensions to what is already in current scope + * + * @param string|array $extensions One or more extensions to add + * @return void + */ + public function addExtensions($extensions) + { + $extensions = array_merge($this->_extensions, (array)$extensions); + $this->_extensions = array_unique($extensions); + } + + /** + * Get the path this scope is for. + * + * @return string + */ + public function path() + { + $routeKey = strpos($this->_path, ':'); + if ($routeKey !== false) { + return substr($this->_path, 0, $routeKey); + } + + return $this->_path; + } + + /** + * Get the parameter names/values for this scope. + * + * @return array + */ + public function params() + { + return $this->_params; + } + + /** + * Checks if there is already a route with a given name. + * + * @param string $name Name. + * @return bool + */ + public function nameExists($name) + { + return array_key_exists($name, $this->_collection->named()); + } + + /** + * Get/set the name prefix for this scope. + * + * Modifying the name prefix will only change the prefix + * used for routes connected after the prefix is changed. + * + * @param string|null $value Either the value to set or null. + * @return string + */ + public function namePrefix($value = null) + { + if ($value !== null) { + $this->_namePrefix = $value; + } + + return $this->_namePrefix; + } + + /** + * Generate REST resource routes for the given controller(s). + * + * A quick way to generate a default routes to a set of REST resources (controller(s)). + * + * ### Usage + * + * Connect resource routes for an app controller: + * + * ``` + * $routes->resources('Posts'); + * ``` + * + * Connect resource routes for the Comments controller in the + * Comments plugin: + * + * ``` + * Router::plugin('Comments', function ($routes) { + * $routes->resources('Comments'); + * }); + * ``` + * + * Plugins will create lower_case underscored resource routes. e.g + * `/comments/comments` + * + * Connect resource routes for the Articles controller in the + * Admin prefix: + * + * ``` + * Router::prefix('admin', function ($routes) { + * $routes->resources('Articles'); + * }); + * ``` + * + * Prefixes will create lower_case underscored resource routes. e.g + * `/admin/posts` + * + * You can create nested resources by passing a callback in: + * + * ``` + * $routes->resources('Articles', function ($routes) { + * $routes->resources('Comments'); + * }); + * ``` + * + * The above would generate both resource routes for `/articles`, and `/articles/:article_id/comments`. + * You can use the `map` option to connect additional resource methods: + * + * ``` + * $routes->resources('Articles', [ + * 'map' => ['deleteAll' => ['action' => 'deleteAll', 'method' => 'DELETE']] + * ]); + * ``` + * + * In addition to the default routes, this would also connect a route for `/articles/delete_all`. + * By default the path segment will match the key name. You can use the 'path' key inside the resource + * definition to customize the path name. + * + * You can use the `inflect` option to change how path segments are generated: + * + * ``` + * $routes->resources('PaymentTypes', ['inflect' => 'dasherize']); + * ``` + * + * Will generate routes like `/payment-types` instead of `/payment_types` + * + * ### Options: + * + * - 'id' - The regular expression fragment to use when matching IDs. By default, matches + * integer values and UUIDs. + * - 'inflect' - Choose the inflection method used on the resource name. Defaults to 'underscore'. + * - 'only' - Only connect the specific list of actions. + * - 'actions' - Override the method names used for connecting actions. + * - 'map' - Additional resource routes that should be connected. If you define 'only' and 'map', + * make sure that your mapped methods are also in the 'only' list. + * - 'prefix' - Define a routing prefix for the resource controller. If the current scope + * defines a prefix, this prefix will be appended to it. + * - 'connectOptions' - Custom options for connecting the routes. + * - 'path' - Change the path so it doesn't match the resource name. E.g ArticlesController + * is available at `/posts` + * + * @param string $name A controller name to connect resource routes for. + * @param array|callable $options Options to use when generating REST routes, or a callback. + * @param callable|null $callback An optional callback to be executed in a nested scope. Nested + * scopes inherit the existing path and 'id' parameter. + * @return void + */ + public function resources($name, $options = [], $callback = null) + { + if (is_callable($options) && $callback === null) { + $callback = $options; + $options = []; + } + $options += [ + 'connectOptions' => [], + 'inflect' => 'underscore', + 'id' => static::ID . '|' . static::UUID, + 'only' => [], + 'actions' => [], + 'map' => [], + 'prefix' => null, + 'path' => null, + ]; + + foreach ($options['map'] as $k => $mapped) { + $options['map'][$k] += ['method' => 'GET', 'path' => $k, 'action' => '']; + } + + $ext = null; + if (!empty($options['_ext'])) { + $ext = $options['_ext']; + } + + $connectOptions = $options['connectOptions']; + if (empty($options['path'])) { + $method = $options['inflect']; + $options['path'] = Inflector::$method($name); + } + $resourceMap = array_merge(static::$_resourceMap, $options['map']); + + $only = (array)$options['only']; + if (empty($only)) { + $only = array_keys($resourceMap); + } + + $prefix = ''; + if ($options['prefix']) { + $prefix = $options['prefix']; + } + if (isset($this->_params['prefix']) && $prefix) { + $prefix = $this->_params['prefix'] . '/' . $prefix; + } + + foreach ($resourceMap as $method => $params) { + if (!in_array($method, $only, true)) { + continue; + } + + $action = $params['action']; + if (isset($options['actions'][$method])) { + $action = $options['actions'][$method]; + } + + $url = '/' . implode('/', array_filter([$options['path'], $params['path']])); + $params = [ + 'controller' => $name, + 'action' => $action, + '_method' => $params['method'], + ]; + if ($prefix) { + $params['prefix'] = $prefix; + } + $routeOptions = $connectOptions + [ + 'id' => $options['id'], + 'pass' => ['id'], + '_ext' => $ext, + ]; + $this->connect($url, $params, $routeOptions); + } + + if (is_callable($callback)) { + $idName = Inflector::singularize(Inflector::underscore($name)) . '_id'; + $path = '/' . $options['path'] . '/:' . $idName; + $this->scope($path, [], $callback); + } + } + + /** + * Create a route that only responds to GET requests. + * + * @param string $template The URL template to use. + * @param array $target An array describing the target route parameters. These parameters + * should indicate the plugin, prefix, controller, and action that this route points to. + * @param string $name The name of the route. + * @return \Cake\Routing\Route\Route + */ + public function get($template, $target, $name = null) + { + return $this->_methodRoute('GET', $template, $target, $name); + } + + /** + * Create a route that only responds to POST requests. + * + * @param string $template The URL template to use. + * @param array $target An array describing the target route parameters. These parameters + * should indicate the plugin, prefix, controller, and action that this route points to. + * @param string $name The name of the route. + * @return \Cake\Routing\Route\Route + */ + public function post($template, $target, $name = null) + { + return $this->_methodRoute('POST', $template, $target, $name); + } + + /** + * Create a route that only responds to PUT requests. + * + * @param string $template The URL template to use. + * @param array $target An array describing the target route parameters. These parameters + * should indicate the plugin, prefix, controller, and action that this route points to. + * @param string $name The name of the route. + * @return \Cake\Routing\Route\Route + */ + public function put($template, $target, $name = null) + { + return $this->_methodRoute('PUT', $template, $target, $name); + } + + /** + * Create a route that only responds to PATCH requests. + * + * @param string $template The URL template to use. + * @param array $target An array describing the target route parameters. These parameters + * should indicate the plugin, prefix, controller, and action that this route points to. + * @param string $name The name of the route. + * @return \Cake\Routing\Route\Route + */ + public function patch($template, $target, $name = null) + { + return $this->_methodRoute('PATCH', $template, $target, $name); + } + + /** + * Create a route that only responds to DELETE requests. + * + * @param string $template The URL template to use. + * @param array $target An array describing the target route parameters. These parameters + * should indicate the plugin, prefix, controller, and action that this route points to. + * @param string $name The name of the route. + * @return \Cake\Routing\Route\Route + */ + public function delete($template, $target, $name = null) + { + return $this->_methodRoute('DELETE', $template, $target, $name); + } + + /** + * Create a route that only responds to HEAD requests. + * + * @param string $template The URL template to use. + * @param array $target An array describing the target route parameters. These parameters + * should indicate the plugin, prefix, controller, and action that this route points to. + * @param string $name The name of the route. + * @return \Cake\Routing\Route\Route + */ + public function head($template, $target, $name = null) + { + return $this->_methodRoute('HEAD', $template, $target, $name); + } + + /** + * Create a route that only responds to OPTIONS requests. + * + * @param string $template The URL template to use. + * @param array $target An array describing the target route parameters. These parameters + * should indicate the plugin, prefix, controller, and action that this route points to. + * @param string $name The name of the route. + * @return \Cake\Routing\Route\Route + */ + public function options($template, $target, $name = null) + { + return $this->_methodRoute('OPTIONS', $template, $target, $name); + } + + /** + * Helper to create routes that only respond to a single HTTP method. + * + * @param string $method The HTTP method name to match. + * @param string $template The URL template to use. + * @param array $target An array describing the target route parameters. These parameters + * should indicate the plugin, prefix, controller, and action that this route points to. + * @param string $name The name of the route. + * @return \Cake\Routing\Route\Route + */ + protected function _methodRoute($method, $template, $target, $name) + { + if ($name !== null) { + $name = $this->_namePrefix . $name; + } + $options = [ + '_name' => $name, + '_ext' => $this->_extensions, + '_middleware' => $this->middleware, + 'routeClass' => $this->_routeClass, + ]; + + $target = $this->parseDefaults($target); + $target['_method'] = $method; + + $route = $this->_makeRoute($template, $target, $options); + $this->_collection->add($route, $options); + + return $route; + } + + /** + * Load routes from a plugin. + * + * The routes file will have a local variable named `$routes` made available which contains + * the current RouteBuilder instance. + * + * @param string $name The plugin name + * @param string $file The routes file to load. Defaults to `routes.php`. This parameter + * is deprecated and will be removed in 4.0 + * @return void + * @throws \Cake\Core\Exception\MissingPluginException When the plugin has not been loaded. + * @throws \InvalidArgumentException When the plugin does not have a routes file. + */ + public function loadPlugin($name, $file = 'routes.php') + { + $plugins = Plugin::getCollection(); + if (!$plugins->has($name)) { + throw new MissingPluginException(['plugin' => $name]); + } + $plugin = $plugins->get($name); + + // @deprecated This block should be removed in 4.0 + if ($file !== 'routes.php') { + deprecationWarning( + 'Loading plugin routes now uses the routes() hook method on the plugin class. ' . + 'Loading non-standard files will be removed in 4.0' + ); + + $path = $plugin->getConfigPath() . DIRECTORY_SEPARATOR . $file; + if (!file_exists($path)) { + throw new InvalidArgumentException(sprintf( + 'Cannot load routes for the plugin named %s. The %s file does not exist.', + $name, + $path + )); + } + + $routes = $this; + include $path; + + return; + } + $plugin->routes($this); + + // Disable the routes hook to prevent duplicate route issues. + $plugin->disable('routes'); + } + + /** + * Connects a new Route. + * + * Routes are a way of connecting request URLs to objects in your application. + * At their core routes are a set or regular expressions that are used to + * match requests to destinations. + * + * Examples: + * + * ``` + * $routes->connect('/:controller/:action/*'); + * ``` + * + * The first parameter will be used as a controller name while the second is + * used as the action name. The '/*' syntax makes this route greedy in that + * it will match requests like `/posts/index` as well as requests + * like `/posts/edit/1/foo/bar`. + * + * ``` + * $routes->connect('/home-page', ['controller' => 'Pages', 'action' => 'display', 'home']); + * ``` + * + * The above shows the use of route parameter defaults. And providing routing + * parameters for a static route. + * + * ``` + * $routes->connect( + * '/:lang/:controller/:action/:id', + * [], + * ['id' => '[0-9]+', 'lang' => '[a-z]{3}'] + * ); + * ``` + * + * Shows connecting a route with custom route parameters as well as + * providing patterns for those parameters. Patterns for routing parameters + * do not need capturing groups, as one will be added for each route params. + * + * $options offers several 'special' keys that have special meaning + * in the $options array. + * + * - `routeClass` is used to extend and change how individual routes parse requests + * and handle reverse routing, via a custom routing class. + * Ex. `'routeClass' => 'SlugRoute'` + * - `pass` is used to define which of the routed parameters should be shifted + * into the pass array. Adding a parameter to pass will remove it from the + * regular route array. Ex. `'pass' => ['slug']`. + * - `persist` is used to define which route parameters should be automatically + * included when generating new URLs. You can override persistent parameters + * by redefining them in a URL or remove them by setting the parameter to `false`. + * Ex. `'persist' => ['lang']` + * - `multibytePattern` Set to true to enable multibyte pattern support in route + * parameter patterns. + * - `_name` is used to define a specific name for routes. This can be used to optimize + * reverse routing lookups. If undefined a name will be generated for each + * connected route. + * - `_ext` is an array of filename extensions that will be parsed out of the url if present. + * See {@link \Cake\Routing\RouteCollection::setExtensions()}. + * - `_method` Only match requests with specific HTTP verbs. + * + * Example of using the `_method` condition: + * + * ``` + * $routes->connect('/tasks', ['controller' => 'Tasks', 'action' => 'index', '_method' => 'GET']); + * ``` + * + * The above route will only be matched for GET requests. POST requests will fail to match this route. + * + * @param string $route A string describing the template of the route + * @param array|string $defaults An array describing the default route parameters. These parameters will be used by default + * and can supply routing parameters that are not dynamic. See above. + * @param array $options An array matching the named elements in the route to regular expressions which that + * element should match. Also contains additional parameters such as which routed parameters should be + * shifted into the passed arguments, supplying patterns for routing parameters and supplying the name of a + * custom routing class. + * @return \Cake\Routing\Route\Route + * @throws \InvalidArgumentException + * @throws \BadMethodCallException + */ + public function connect($route, $defaults = [], array $options = []) + { + $defaults = $this->parseDefaults($defaults); + if (!isset($options['action']) && !isset($defaults['action'])) { + $defaults['action'] = 'index'; + } + + if (empty($options['_ext'])) { + $options['_ext'] = $this->_extensions; + } + + if (empty($options['routeClass'])) { + $options['routeClass'] = $this->_routeClass; + } + if (isset($options['_name']) && $this->_namePrefix) { + $options['_name'] = $this->_namePrefix . $options['_name']; + } + if (empty($options['_middleware'])) { + $options['_middleware'] = $this->middleware; + } + + $route = $this->_makeRoute($route, $defaults, $options); + $this->_collection->add($route, $options); + + return $route; + } + + /** + * Parse the defaults if they're a string + * + * @param string|array $defaults Defaults array from the connect() method. + * @return string|array + */ + protected static function parseDefaults($defaults) + { + if (!is_string($defaults)) { + return $defaults; + } + + $regex = '/(?:(?[a-zA-Z0-9\/]*)\.)?(?[a-zA-Z0-9\/]*?)' . + '(?:\/)?(?[a-zA-Z0-9]*):{2}(?[a-zA-Z0-9_]*)/i'; + + if (preg_match($regex, $defaults, $matches)) { + foreach ($matches as $key => $value) { + // Remove numeric keys and empty values. + if (is_int($key) || $value === '' || $value === '::') { + unset($matches[$key]); + } + } + $length = count($matches); + + if (isset($matches['prefix'])) { + $matches['prefix'] = strtolower($matches['prefix']); + } + + if ($length >= 2 || $length <= 4) { + return $matches; + } + } + throw new RuntimeException("Could not parse `{$defaults}` route destination string."); + } + + /** + * Create a route object, or return the provided object. + * + * @param string|\Cake\Routing\Route\Route $route The route template or route object. + * @param array $defaults Default parameters. + * @param array $options Additional options parameters. + * @return \Cake\Routing\Route\Route + * @throws \InvalidArgumentException when route class or route object is invalid. + * @throws \BadMethodCallException when the route to make conflicts with the current scope + */ + protected function _makeRoute($route, $defaults, $options) + { + if (is_string($route)) { + $routeClass = App::className($options['routeClass'], 'Routing/Route'); + if ($routeClass === false) { + throw new InvalidArgumentException(sprintf( + 'Cannot find route class %s', + $options['routeClass'] + )); + } + + $route = str_replace('//', '/', $this->_path . $route); + if ($route !== '/') { + $route = rtrim($route, '/'); + } + + foreach ($this->_params as $param => $val) { + if (isset($defaults[$param]) && $param !== 'prefix' && $defaults[$param] !== $val) { + $msg = 'You cannot define routes that conflict with the scope. ' . + 'Scope had %s = %s, while route had %s = %s'; + throw new BadMethodCallException(sprintf( + $msg, + $param, + $val, + $param, + $defaults[$param] + )); + } + } + $defaults += $this->_params + ['plugin' => null]; + + $route = new $routeClass($route, $defaults, $options); + } + + if ($route instanceof Route) { + return $route; + } + throw new InvalidArgumentException( + 'Route class not found, or route class is not a subclass of Cake\Routing\Route\Route' + ); + } + + /** + * Connects a new redirection Route in the router. + * + * Redirection routes are different from normal routes as they perform an actual + * header redirection if a match is found. The redirection can occur within your + * application or redirect to an outside location. + * + * Examples: + * + * ``` + * $routes->redirect('/home/*', ['controller' => 'posts', 'action' => 'view']); + * ``` + * + * Redirects /home/* to /posts/view and passes the parameters to /posts/view. Using an array as the + * redirect destination allows you to use other routes to define where a URL string should be redirected to. + * + * ``` + * $routes->redirect('/posts/*', 'http://google.com', ['status' => 302]); + * ``` + * + * Redirects /posts/* to http://google.com with a HTTP status of 302 + * + * ### Options: + * + * - `status` Sets the HTTP status (default 301) + * - `persist` Passes the params to the redirected route, if it can. This is useful with greedy routes, + * routes that end in `*` are greedy. As you can remap URLs and not lose any passed args. + * + * @param string $route A string describing the template of the route + * @param array|string $url A URL to redirect to. Can be a string or a Cake array-based URL + * @param array $options An array matching the named elements in the route to regular expressions which that + * element should match. Also contains additional parameters such as which routed parameters should be + * shifted into the passed arguments. As well as supplying patterns for routing parameters. + * @return void + */ + public function redirect($route, $url, array $options = []) + { + if (!isset($options['routeClass'])) { + $options['routeClass'] = 'Cake\Routing\Route\RedirectRoute'; + } + if (is_string($url)) { + $url = ['redirect' => $url]; + } + $this->connect($route, $url, $options); + } + + /** + * Add prefixed routes. + * + * This method creates a scoped route collection that includes + * relevant prefix information. + * + * The $name parameter is used to generate the routing parameter name. + * For example a path of `admin` would result in `'prefix' => 'admin'` being + * applied to all connected routes. + * + * You can re-open a prefix as many times as necessary, as well as nest prefixes. + * Nested prefixes will result in prefix values like `admin/api` which translates + * to the `Controller\Admin\Api\` namespace. + * + * If you need to have prefix with dots, eg: '/api/v1.0', use 'path' key + * for $params argument: + * + * ``` + * $route->prefix('api', function($route) { + * $route->prefix('v10', ['path' => '/v1.0'], function($route) { + * // Translates to `Controller\Api\V10\` namespace + * }); + * }); + * ``` + * + * @param string $name The prefix name to use. + * @param array|callable $params An array of routing defaults to add to each connected route. + * If you have no parameters, this argument can be a callable. + * @param callable|null $callback The callback to invoke that builds the prefixed routes. + * @return void + * @throws \InvalidArgumentException If a valid callback is not passed + */ + public function prefix($name, $params = [], callable $callback = null) + { + if ($callback === null) { + if (!is_callable($params)) { + throw new InvalidArgumentException('A valid callback is expected'); + } + $callback = $params; + $params = []; + } + $name = Inflector::underscore($name); + $path = '/' . $name; + if (isset($params['path'])) { + $path = $params['path']; + unset($params['path']); + } + if (isset($this->_params['prefix'])) { + $name = $this->_params['prefix'] . '/' . $name; + } + $params = array_merge($params, ['prefix' => $name]); + $this->scope($path, $params, $callback); + } + + /** + * Add plugin routes. + * + * This method creates a new scoped route collection that includes + * relevant plugin information. + * + * The plugin name will be inflected to the underscore version to create + * the routing path. If you want a custom path name, use the `path` option. + * + * Routes connected in the scoped collection will have the correct path segment + * prepended, and have a matching plugin routing key set. + * + * @param string $name The plugin name to build routes for + * @param array|callable $options Either the options to use, or a callback + * @param callable|null $callback The callback to invoke that builds the plugin routes + * Only required when $options is defined. + * @return void + */ + public function plugin($name, $options = [], $callback = null) + { + if ($callback === null) { + $callback = $options; + $options = []; + } + $params = ['plugin' => $name] + $this->_params; + if (empty($options['path'])) { + $options['path'] = '/' . Inflector::underscore($name); + } + $this->scope($options['path'], $params, $callback); + } + + /** + * Create a new routing scope. + * + * Scopes created with this method will inherit the properties of the scope they are + * added to. This means that both the current path and parameters will be appended + * to the supplied parameters. + * + * @param string $path The path to create a scope for. + * @param array|callable $params Either the parameters to add to routes, or a callback. + * @param callable|null $callback The callback to invoke that builds the plugin routes. + * Only required when $params is defined. + * @return void + * @throws \InvalidArgumentException when there is no callable parameter. + */ + public function scope($path, $params, $callback = null) + { + if ($callback === null) { + $callback = $params; + $params = []; + } + if (!is_callable($callback)) { + $msg = 'Need a callable function/object to connect routes.'; + throw new InvalidArgumentException($msg); + } + + if ($this->_path !== '/') { + $path = $this->_path . $path; + } + $namePrefix = $this->_namePrefix; + if (isset($params['_namePrefix'])) { + $namePrefix .= $params['_namePrefix']; + } + unset($params['_namePrefix']); + + $params += $this->_params; + $builder = new static($this->_collection, $path, $params, [ + 'routeClass' => $this->_routeClass, + 'extensions' => $this->_extensions, + 'namePrefix' => $namePrefix, + 'middleware' => $this->middleware, + ]); + $callback($builder); + } + + /** + * Connect the `/:controller` and `/:controller/:action/*` fallback routes. + * + * This is a shortcut method for connecting fallback routes in a given scope. + * + * @param string|null $routeClass the route class to use, uses the default routeClass + * if not specified + * @return void + */ + public function fallbacks($routeClass = null) + { + $routeClass = $routeClass ?: $this->_routeClass; + $this->connect('/:controller', ['action' => 'index'], compact('routeClass')); + $this->connect('/:controller/:action/*', [], compact('routeClass')); + } + + /** + * Register a middleware with the RouteCollection. + * + * Once middleware has been registered, it can be applied to the current routing + * scope or any child scopes that share the same RouteCollection. + * + * @param string $name The name of the middleware. Used when applying middleware to a scope. + * @param callable|string $middleware The middleware callable or class name to register. + * @return $this + * @see \Cake\Routing\RouteCollection + */ + public function registerMiddleware($name, $middleware) + { + $this->_collection->registerMiddleware($name, $middleware); + + return $this; + } + + /** + * Apply a middleware to the current route scope. + * + * Requires middleware to be registered via `registerMiddleware()` + * + * @param string ...$names The names of the middleware to apply to the current scope. + * @return $this + * @see \Cake\Routing\RouteCollection::addMiddlewareToScope() + */ + public function applyMiddleware(...$names) + { + foreach ($names as $name) { + if (!$this->_collection->middlewareExists($name)) { + $message = "Cannot apply '$name' middleware or middleware group. " . + 'Use registerMiddleware() to register middleware.'; + throw new RuntimeException($message); + } + } + $this->middleware = array_merge($this->middleware, $names); + + return $this; + } + + /** + * Apply a set of middleware to a group + * + * @param string $name Name of the middleware group + * @param array $middlewareNames Names of the middleware + * @return $this + */ + public function middlewareGroup($name, array $middlewareNames) + { + $this->_collection->middlewareGroup($name, $middlewareNames); + + return $this; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/RouteCollection.php b/app/vendor/cakephp/cakephp/src/Routing/RouteCollection.php new file mode 100644 index 000000000..30a6f6705 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/RouteCollection.php @@ -0,0 +1,552 @@ +_routes[] = $route; + + // Explicit names + if (isset($options['_name'])) { + if (isset($this->_named[$options['_name']])) { + $matched = $this->_named[$options['_name']]; + throw new DuplicateNamedRouteException([ + 'name' => $options['_name'], + 'url' => $matched->template, + 'duplicate' => $matched, + ]); + } + $this->_named[$options['_name']] = $route; + } + + // Generated names. + $name = $route->getName(); + if (!isset($this->_routeTable[$name])) { + $this->_routeTable[$name] = []; + } + $this->_routeTable[$name][] = $route; + + // Index path prefixes (for parsing) + $path = $route->staticPath(); + $this->_paths[$path][] = $route; + + $extensions = $route->getExtensions(); + if (count($extensions) > 0) { + $this->setExtensions($extensions); + } + } + + /** + * Takes the URL string and iterates the routes until one is able to parse the route. + * + * @param string $url URL to parse. + * @param string $method The HTTP method to use. + * @return array An array of request parameters parsed from the URL. + * @throws \Cake\Routing\Exception\MissingRouteException When a URL has no matching route. + */ + public function parse($url, $method = '') + { + $decoded = urldecode($url); + + // Sort path segments matching longest paths first. + $paths = array_keys($this->_paths); + rsort($paths); + + foreach ($paths as $path) { + if (strpos($decoded, $path) !== 0) { + continue; + } + + $queryParameters = null; + if (strpos($url, '?') !== false) { + list($url, $queryParameters) = explode('?', $url, 2); + parse_str($queryParameters, $queryParameters); + } + /* @var \Cake\Routing\Route\Route $route */ + foreach ($this->_paths[$path] as $route) { + $r = $route->parse($url, $method); + if ($r === false) { + continue; + } + if ($queryParameters) { + $r['?'] = $queryParameters; + } + + return $r; + } + } + + $exceptionProperties = ['url' => $url]; + if ($method !== '') { + // Ensure that if the method is included, it is the first element of + // the array, to match the order that the strings are printed in the + // MissingRouteException error message, $_messageTemplateWithMethod. + $exceptionProperties = array_merge(['method' => $method], $exceptionProperties); + } + throw new MissingRouteException($exceptionProperties); + } + + /** + * Takes the ServerRequestInterface, iterates the routes until one is able to parse the route. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request to parse route data from. + * @return array An array of request parameters parsed from the URL. + * @throws \Cake\Routing\Exception\MissingRouteException When a URL has no matching route. + */ + public function parseRequest(ServerRequestInterface $request) + { + $uri = $request->getUri(); + $urlPath = urldecode($uri->getPath()); + + // Sort path segments matching longest paths first. + $paths = array_keys($this->_paths); + rsort($paths); + + foreach ($paths as $path) { + if (strpos($urlPath, $path) !== 0) { + continue; + } + + /* @var \Cake\Routing\Route\Route $route */ + foreach ($this->_paths[$path] as $route) { + $r = $route->parseRequest($request); + if ($r === false) { + continue; + } + if ($uri->getQuery()) { + parse_str($uri->getQuery(), $queryParameters); + $r['?'] = $queryParameters; + } + + return $r; + } + } + throw new MissingRouteException(['url' => $urlPath]); + } + + /** + * Get the set of names from the $url. Accepts both older style array urls, + * and newer style urls containing '_name' + * + * @param array $url The url to match. + * @return array The set of names of the url + */ + protected function _getNames($url) + { + $plugin = false; + if (isset($url['plugin']) && $url['plugin'] !== false) { + $plugin = strtolower($url['plugin']); + } + $prefix = false; + if (isset($url['prefix']) && $url['prefix'] !== false) { + $prefix = strtolower($url['prefix']); + } + $controller = strtolower($url['controller']); + $action = strtolower($url['action']); + + $names = [ + "${controller}:${action}", + "${controller}:_action", + "_controller:${action}", + '_controller:_action', + ]; + + // No prefix, no plugin + if ($prefix === false && $plugin === false) { + return $names; + } + + // Only a plugin + if ($prefix === false) { + return [ + "${plugin}.${controller}:${action}", + "${plugin}.${controller}:_action", + "${plugin}._controller:${action}", + "${plugin}._controller:_action", + "_plugin.${controller}:${action}", + "_plugin.${controller}:_action", + "_plugin._controller:${action}", + '_plugin._controller:_action', + ]; + } + + // Only a prefix + if ($plugin === false) { + return [ + "${prefix}:${controller}:${action}", + "${prefix}:${controller}:_action", + "${prefix}:_controller:${action}", + "${prefix}:_controller:_action", + "_prefix:${controller}:${action}", + "_prefix:${controller}:_action", + "_prefix:_controller:${action}", + '_prefix:_controller:_action', + ]; + } + + // Prefix and plugin has the most options + // as there are 4 factors. + return [ + "${prefix}:${plugin}.${controller}:${action}", + "${prefix}:${plugin}.${controller}:_action", + "${prefix}:${plugin}._controller:${action}", + "${prefix}:${plugin}._controller:_action", + "${prefix}:_plugin.${controller}:${action}", + "${prefix}:_plugin.${controller}:_action", + "${prefix}:_plugin._controller:${action}", + "${prefix}:_plugin._controller:_action", + "_prefix:${plugin}.${controller}:${action}", + "_prefix:${plugin}.${controller}:_action", + "_prefix:${plugin}._controller:${action}", + "_prefix:${plugin}._controller:_action", + "_prefix:_plugin.${controller}:${action}", + "_prefix:_plugin.${controller}:_action", + "_prefix:_plugin._controller:${action}", + '_prefix:_plugin._controller:_action', + ]; + } + + /** + * Reverse route or match a $url array with the connected routes. + * + * Returns either the URL string generated by the route, + * or throws an exception on failure. + * + * @param array $url The URL to match. + * @param array $context The request context to use. Contains _base, _port, + * _host, _scheme and params keys. + * @return string The URL string on match. + * @throws \Cake\Routing\Exception\MissingRouteException When no route could be matched. + */ + public function match($url, $context) + { + // Named routes support optimization. + if (isset($url['_name'])) { + $name = $url['_name']; + unset($url['_name']); + if (isset($this->_named[$name])) { + $route = $this->_named[$name]; + $out = $route->match($url + $route->defaults, $context); + if ($out) { + return $out; + } + throw new MissingRouteException([ + 'url' => $name, + 'context' => $context, + 'message' => 'A named route was found for "%s", but matching failed.', + ]); + } + throw new MissingRouteException(['url' => $name, 'context' => $context]); + } + + foreach ($this->_getNames($url) as $name) { + if (empty($this->_routeTable[$name])) { + continue; + } + /* @var \Cake\Routing\Route\Route $route */ + foreach ($this->_routeTable[$name] as $route) { + $match = $route->match($url, $context); + if ($match) { + return strlen($match) > 1 ? trim($match, '/') : $match; + } + } + } + throw new MissingRouteException(['url' => var_export($url, true), 'context' => $context]); + } + + /** + * Get all the connected routes as a flat list. + * + * @return \Cake\Routing\Route\Route[] + */ + public function routes() + { + return $this->_routes; + } + + /** + * Get the connected named routes. + * + * @return \Cake\Routing\Route\Route[] + */ + public function named() + { + return $this->_named; + } + + /** + * Get/set the extensions that the route collection could handle. + * + * @param null|string|array $extensions Either the list of extensions to set, + * or null to get. + * @param bool $merge Whether to merge with or override existing extensions. + * Defaults to `true`. + * @return array The valid extensions. + * @deprecated 3.5.0 Use getExtensions()/setExtensions() instead. + */ + public function extensions($extensions = null, $merge = true) + { + deprecationWarning( + 'RouteCollection::extensions() is deprecated. ' . + 'Use RouteCollection::setExtensions()/getExtensions() instead.' + ); + if ($extensions !== null) { + $this->setExtensions((array)$extensions, $merge); + } + + return $this->getExtensions(); + } + + /** + * Get the extensions that can be handled. + * + * @return array The valid extensions. + */ + public function getExtensions() + { + return $this->_extensions; + } + + /** + * Set the extensions that the route collection can handle. + * + * @param array $extensions The list of extensions to set. + * @param bool $merge Whether to merge with or override existing extensions. + * Defaults to `true`. + * @return $this + */ + public function setExtensions(array $extensions, $merge = true) + { + if ($merge) { + $extensions = array_unique(array_merge( + $this->_extensions, + $extensions + )); + } + $this->_extensions = $extensions; + + return $this; + } + + /** + * Register a middleware with the RouteCollection. + * + * Once middleware has been registered, it can be applied to the current routing + * scope or any child scopes that share the same RouteCollection. + * + * @param string $name The name of the middleware. Used when applying middleware to a scope. + * @param callable|string $middleware The middleware callable or class name to register. + * @return $this + */ + public function registerMiddleware($name, $middleware) + { + $this->_middleware[$name] = $middleware; + + return $this; + } + + /** + * Add middleware to a middleware group + * + * @param string $name Name of the middleware group + * @param array $middlewareNames Names of the middleware + * @return $this + */ + public function middlewareGroup($name, array $middlewareNames) + { + if ($this->hasMiddleware($name)) { + $message = "Cannot add middleware group '$name'. A middleware by this name has already been registered."; + throw new RuntimeException($message); + } + + foreach ($middlewareNames as $middlewareName) { + if (!$this->hasMiddleware($middlewareName)) { + $message = "Cannot add '$middlewareName' middleware to group '$name'. It has not been registered."; + throw new RuntimeException($message); + } + } + + $this->_middlewareGroups[$name] = $middlewareNames; + + return $this; + } + + /** + * Check if the named middleware group has been created. + * + * @param string $name The name of the middleware group to check. + * @return bool + */ + public function hasMiddlewareGroup($name) + { + return array_key_exists($name, $this->_middlewareGroups); + } + + /** + * Check if the named middleware has been registered. + * + * @param string $name The name of the middleware to check. + * @return bool + */ + public function hasMiddleware($name) + { + return isset($this->_middleware[$name]); + } + + /** + * Check if the named middleware or middleware group has been registered. + * + * @param string $name The name of the middleware to check. + * @return bool + */ + public function middlewareExists($name) + { + return $this->hasMiddleware($name) || $this->hasMiddlewareGroup($name); + } + + /** + * Apply a registered middleware(s) for the provided path + * + * @param string $path The URL path to register middleware for. + * @param string[] $middleware The middleware names to add for the path. + * @return $this + */ + public function applyMiddleware($path, array $middleware) + { + foreach ($middleware as $name) { + if (!$this->hasMiddleware($name) && !$this->hasMiddlewareGroup($name)) { + $message = "Cannot apply '$name' middleware or middleware group to path '$path'. It has not been registered."; + throw new RuntimeException($message); + } + } + // Matches route element pattern in Cake\Routing\Route + $path = '#^' . preg_quote($path, '#') . '#'; + $path = preg_replace('/\\\\:([a-z0-9-_]+(?_middlewarePaths[$path])) { + $this->_middlewarePaths[$path] = []; + } + $this->_middlewarePaths[$path] = array_merge($this->_middlewarePaths[$path], $middleware); + + return $this; + } + + /** + * Get an array of middleware given a list of names + * + * @param array $names The names of the middleware or groups to fetch + * @return array An array of middleware. If any of the passed names are groups, + * the groups middleware will be flattened into the returned list. + * @throws \RuntimeException when a requested middleware does not exist. + */ + public function getMiddleware(array $names) + { + $out = []; + foreach ($names as $name) { + if ($this->hasMiddlewareGroup($name)) { + $out = array_merge($out, $this->getMiddleware($this->_middlewareGroups[$name])); + continue; + } + if (!$this->hasMiddleware($name)) { + $message = "The middleware named '$name' has not been registered. Use registerMiddleware() to define it."; + throw new RuntimeException($message); + } + $out[] = $this->_middleware[$name]; + } + + return $out; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Routing/Router.php b/app/vendor/cakephp/cakephp/src/Routing/Router.php new file mode 100644 index 000000000..6617298c7 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Routing/Router.php @@ -0,0 +1,1173 @@ + Router::ACTION, + 'Year' => Router::YEAR, + 'Month' => Router::MONTH, + 'Day' => Router::DAY, + 'ID' => Router::ID, + 'UUID' => Router::UUID + ]; + + /** + * Maintains the request object stack for the current request. + * This will contain more than one request object when requestAction is used. + * + * @var array + */ + protected static $_requests = []; + + /** + * Initial state is populated the first time reload() is called which is at the bottom + * of this file. This is a cheat as get_class_vars() returns the value of static vars even if they + * have changed. + * + * @var array + */ + protected static $_initialState = []; + + /** + * The stack of URL filters to apply against routing URLs before passing the + * parameters to the route collection. + * + * @var callable[] + */ + protected static $_urlFilters = []; + + /** + * Default extensions defined with Router::extensions() + * + * @var array + */ + protected static $_defaultExtensions = []; + + /** + * Get or set default route class. + * + * @param string|null $routeClass Class name. + * @return string|null + */ + public static function defaultRouteClass($routeClass = null) + { + if ($routeClass === null) { + return static::$_defaultRouteClass; + } + static::$_defaultRouteClass = $routeClass; + } + + /** + * Gets the named route patterns for use in config/routes.php + * + * @return array Named route elements + * @see \Cake\Routing\Router::$_namedExpressions + */ + public static function getNamedExpressions() + { + return static::$_namedExpressions; + } + + /** + * Connects a new Route in the router. + * + * Compatibility proxy to \Cake\Routing\RouteBuilder::connect() in the `/` scope. + * + * @param string $route A string describing the template of the route + * @param array|string $defaults An array describing the default route parameters. These parameters will be used by default + * and can supply routing parameters that are not dynamic. See above. + * @param array $options An array matching the named elements in the route to regular expressions which that + * element should match. Also contains additional parameters such as which routed parameters should be + * shifted into the passed arguments, supplying patterns for routing parameters and supplying the name of a + * custom routing class. + * @return void + * @throws \Cake\Core\Exception\Exception + * @see \Cake\Routing\RouteBuilder::connect() + * @see \Cake\Routing\Router::scope() + */ + public static function connect($route, $defaults = [], $options = []) + { + static::$initialized = true; + static::scope('/', function ($routes) use ($route, $defaults, $options) { + $routes->connect($route, $defaults, $options); + }); + } + + /** + * Connects a new redirection Route in the router. + * + * Compatibility proxy to \Cake\Routing\RouteBuilder::redirect() in the `/` scope. + * + * @param string $route A string describing the template of the route + * @param array $url A URL to redirect to. Can be a string or a Cake array-based URL + * @param array $options An array matching the named elements in the route to regular expressions which that + * element should match. Also contains additional parameters such as which routed parameters should be + * shifted into the passed arguments. As well as supplying patterns for routing parameters. + * @return void + * @see \Cake\Routing\RouteBuilder::redirect() + * @deprecated 3.3.0 Use Router::scope() and RouteBuilder::redirect() instead. + */ + public static function redirect($route, $url, $options = []) + { + deprecationWarning( + 'Router::redirect() is deprecated. ' . + 'Use Router::scope() and RouteBuilder::redirect() instead.' + ); + if (is_string($url)) { + $url = ['redirect' => $url]; + } + if (!isset($options['routeClass'])) { + $options['routeClass'] = 'Cake\Routing\Route\RedirectRoute'; + } + static::connect($route, $url, $options); + } + + /** + * Generate REST resource routes for the given controller(s). + * + * Compatibility proxy to \Cake\Routing\RouteBuilder::resources(). Additional, compatibility + * around prefixes and plugins and prefixes is handled by this method. + * + * A quick way to generate a default routes to a set of REST resources (controller(s)). + * + * ### Usage + * + * Connect resource routes for an app controller: + * + * ``` + * Router::mapResources('Posts'); + * ``` + * + * Connect resource routes for the Comment controller in the + * Comments plugin: + * + * ``` + * Router::mapResources('Comments.Comment'); + * ``` + * + * Plugins will create lower_case underscored resource routes. e.g + * `/comments/comment` + * + * Connect resource routes for the Posts controller in the + * Admin prefix: + * + * ``` + * Router::mapResources('Posts', ['prefix' => 'admin']); + * ``` + * + * Prefixes will create lower_case underscored resource routes. e.g + * `/admin/posts` + * + * ### Options: + * + * - 'id' - The regular expression fragment to use when matching IDs. By default, matches + * integer values and UUIDs. + * - 'prefix' - Routing prefix to use for the generated routes. Defaults to ''. + * Using this option will create prefixed routes, similar to using Routing.prefixes. + * - 'only' - Only connect the specific list of actions. + * - 'actions' - Override the method names used for connecting actions. + * - 'map' - Additional resource routes that should be connected. If you define 'only' and 'map', + * make sure that your mapped methods are also in the 'only' list. + * - 'path' - Change the path so it doesn't match the resource name. E.g ArticlesController + * is available at `/posts` + * + * @param string|array $controller A controller name or array of controller names (i.e. "Posts" or "ListItems") + * @param array $options Options to use when generating REST routes + * @see \Cake\Routing\RouteBuilder::resources() + * @deprecated 3.3.0 Use Router::scope() and RouteBuilder::resources() instead. + * @return void + */ + public static function mapResources($controller, $options = []) + { + deprecationWarning( + 'Router::mapResources() is deprecated. ' . + 'Use Router::scope() and RouteBuilder::resources() instead.' + ); + foreach ((array)$controller as $name) { + list($plugin, $name) = pluginSplit($name); + + $prefix = $pluginUrl = false; + if (!empty($options['prefix'])) { + $prefix = $options['prefix']; + unset($options['prefix']); + } + if ($plugin) { + $pluginUrl = Inflector::underscore($plugin); + } + + $callback = function ($routes) use ($name, $options) { + $routes->resources($name, $options); + }; + + if ($plugin && $prefix) { + $path = '/' . implode('/', [$prefix, $pluginUrl]); + $params = ['prefix' => $prefix, 'plugin' => $plugin]; + static::scope($path, $params, $callback); + + return; + } + + if ($prefix) { + static::prefix($prefix, $callback); + + return; + } + + if ($plugin) { + static::plugin($plugin, $callback); + + return; + } + + static::scope('/', $callback); + + return; + } + } + + /** + * Parses given URL string. Returns 'routing' parameters for that URL. + * + * @param string $url URL to be parsed. + * @param string $method The HTTP method being used. + * @return array Parsed elements from URL. + * @throws \Cake\Routing\Exception\MissingRouteException When a route cannot be handled + * @deprecated 3.4.0 Use Router::parseRequest() instead. + */ + public static function parse($url, $method = '') + { + deprecationWarning( + 'Router::parse() is deprecated. ' . + 'Use Router::parseRequest() instead. This will require adopting the Http\Server library.' + ); + if (!static::$initialized) { + static::_loadRoutes(); + } + if (strpos($url, '/') !== 0) { + $url = '/' . $url; + } + + return static::$_collection->parse($url, $method); + } + + /** + * Get the routing parameters for the request is possible. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request to parse request data from. + * @return array Parsed elements from URL. + * @throws \Cake\Routing\Exception\MissingRouteException When a route cannot be handled + */ + public static function parseRequest(ServerRequestInterface $request) + { + if (!static::$initialized) { + static::_loadRoutes(); + } + + return static::$_collection->parseRequest($request); + } + + /** + * Takes parameter and path information back from the Dispatcher, sets these + * parameters as the current request parameters that are merged with URL arrays + * created later in the request. + * + * Nested requests will create a stack of requests. You can remove requests using + * Router::popRequest(). This is done automatically when using Object::requestAction(). + * + * Will accept either a Cake\Http\ServerRequest object or an array of arrays. Support for + * accepting arrays may be removed in the future. + * + * @param \Cake\Http\ServerRequest|array $request Parameters and path information or a Cake\Http\ServerRequest object. + * @return void + * @deprecatd 3.6.0 Support for arrays will be removed in 4.0.0 + */ + public static function setRequestInfo($request) + { + if ($request instanceof ServerRequest) { + static::pushRequest($request); + } else { + deprecationWarning( + 'Passing an array into Router::setRequestInfo() is deprecated. ' . + 'Pass an instance of ServerRequest instead.' + ); + + $requestData = $request; + $requestData += [[], []]; + $requestData[0] += [ + 'controller' => false, + 'action' => false, + 'plugin' => null + ]; + $request = new ServerRequest([ + 'params' => $requestData[0], + 'url' => isset($requestData[1]['here']) ? $requestData[1]['here'] : '/', + 'base' => isset($requestData[1]['base']) ? $requestData[1]['base'] : '', + 'webroot' => isset($requestData[1]['webroot']) ? $requestData[1]['webroot'] : '/', + ]); + static::pushRequest($request); + } + } + + /** + * Push a request onto the request stack. Pushing a request + * sets the request context used when generating URLs. + * + * @param \Cake\Http\ServerRequest $request Request instance. + * @return void + */ + public static function pushRequest(ServerRequest $request) + { + static::$_requests[] = $request; + static::setRequestContext($request); + } + + /** + * Store the request context for a given request. + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request instance. + * @return void + * @throws \InvalidArgumentException When parameter is an incorrect type. + */ + public static function setRequestContext(ServerRequestInterface $request) + { + $uri = $request->getUri(); + static::$_requestContext = [ + '_base' => $request->getAttribute('base'), + '_port' => $uri->getPort(), + '_scheme' => $uri->getScheme(), + '_host' => $uri->getHost(), + ]; + } + + /** + * Pops a request off of the request stack. Used when doing requestAction + * + * @return \Cake\Http\ServerRequest The request removed from the stack. + * @see \Cake\Routing\Router::pushRequest() + * @see \Cake\Routing\RequestActionTrait::requestAction() + */ + public static function popRequest() + { + $removed = array_pop(static::$_requests); + $last = end(static::$_requests); + if ($last) { + static::setRequestContext($last); + reset(static::$_requests); + } + + return $removed; + } + + /** + * Get the current request object, or the first one. + * + * @param bool $current True to get the current request, or false to get the first one. + * @return \Cake\Http\ServerRequest|null + */ + public static function getRequest($current = false) + { + if ($current) { + $request = end(static::$_requests); + + return $request ?: null; + } + + return isset(static::$_requests[0]) ? static::$_requests[0] : null; + } + + /** + * Reloads default Router settings. Resets all class variables and + * removes all connected routes. + * + * @return void + */ + public static function reload() + { + if (empty(static::$_initialState)) { + static::$_collection = new RouteCollection(); + static::$_initialState = get_class_vars(get_called_class()); + + return; + } + foreach (static::$_initialState as $key => $val) { + if ($key !== '_initialState') { + static::${$key} = $val; + } + } + static::$_collection = new RouteCollection(); + } + + /** + * Add a URL filter to Router. + * + * URL filter functions are applied to every array $url provided to + * Router::url() before the URLs are sent to the route collection. + * + * Callback functions should expect the following parameters: + * + * - `$params` The URL params being processed. + * - `$request` The current request. + * + * The URL filter function should *always* return the params even if unmodified. + * + * ### Usage + * + * URL filters allow you to easily implement features like persistent parameters. + * + * ``` + * Router::addUrlFilter(function ($params, $request) { + * if ($request->getParam('lang') && !isset($params['lang'])) { + * $params['lang'] = $request->getParam('lang'); + * } + * return $params; + * }); + * ``` + * + * @param callable $function The function to add + * @return void + */ + public static function addUrlFilter(callable $function) + { + static::$_urlFilters[] = $function; + } + + /** + * Applies all the connected URL filters to the URL. + * + * @param array $url The URL array being modified. + * @return array The modified URL. + * @see \Cake\Routing\Router::url() + * @see \Cake\Routing\Router::addUrlFilter() + */ + protected static function _applyUrlFilters($url) + { + $request = static::getRequest(true); + foreach (static::$_urlFilters as $filter) { + $url = $filter($url, $request); + } + + return $url; + } + + /** + * Finds URL for specified action. + * + * Returns a URL pointing to a combination of controller and action. + * + * ### Usage + * + * - `Router::url('/posts/edit/1');` Returns the string with the base dir prepended. + * This usage does not use reverser routing. + * - `Router::url(['controller' => 'posts', 'action' => 'edit']);` Returns a URL + * generated through reverse routing. + * - `Router::url(['_name' => 'custom-name', ...]);` Returns a URL generated + * through reverse routing. This form allows you to leverage named routes. + * + * There are a few 'special' parameters that can change the final URL string that is generated + * + * - `_base` - Set to false to remove the base path from the generated URL. If your application + * is not in the root directory, this can be used to generate URLs that are 'cake relative'. + * cake relative URLs are required when using requestAction. + * - `_scheme` - Set to create links on different schemes like `webcal` or `ftp`. Defaults + * to the current scheme. + * - `_host` - Set the host to use for the link. Defaults to the current host. + * - `_port` - Set the port if you need to create links on non-standard ports. + * - `_full` - If true output of `Router::fullBaseUrl()` will be prepended to generated URLs. + * - `#` - Allows you to set URL hash fragments. + * - `_ssl` - Set to true to convert the generated URL to https, or false to force http. + * - `_name` - Name of route. If you have setup named routes you can use this key + * to specify it. + * + * @param string|array|null $url An array specifying any of the following: + * 'controller', 'action', 'plugin' additionally, you can provide routed + * elements or query string parameters. If string it can be name any valid url + * string. + * @param bool $full If true, the full base URL will be prepended to the result. + * Default is false. + * @return string Full translated URL with base path. + * @throws \Cake\Core\Exception\Exception When the route name is not found + */ + public static function url($url = null, $full = false) + { + if (!static::$initialized) { + static::_loadRoutes(); + } + + $params = [ + 'plugin' => null, + 'controller' => null, + 'action' => 'index', + '_ext' => null, + ]; + $here = $base = $output = $frag = null; + + // In 4.x this should be replaced with state injected via setRequestContext + $request = static::getRequest(true); + if ($request) { + $params = $request->getAttribute('params'); + $here = $request->getRequestTarget(); + $base = $request->getAttribute('base'); + } else { + $base = Configure::read('App.base'); + if (isset(static::$_requestContext['_base'])) { + $base = static::$_requestContext['_base']; + } + } + + if (empty($url)) { + $output = $base . (isset($here) ? $here : '/'); + if ($full) { + $output = static::fullBaseUrl() . $output; + } + + return $output; + } + if (is_array($url)) { + if (isset($url['_ssl'])) { + $url['_scheme'] = ($url['_ssl'] === true) ? 'https' : 'http'; + } + + if (isset($url['_full']) && $url['_full'] === true) { + $full = true; + } + if (isset($url['#'])) { + $frag = '#' . $url['#']; + } + unset($url['_ssl'], $url['_full'], $url['#']); + + $url = static::_applyUrlFilters($url); + + if (!isset($url['_name'])) { + // Copy the current action if the controller is the current one. + if (empty($url['action']) && + (empty($url['controller']) || $params['controller'] === $url['controller']) + ) { + $url['action'] = $params['action']; + } + + // Keep the current prefix around if none set. + if (isset($params['prefix']) && !isset($url['prefix'])) { + $url['prefix'] = $params['prefix']; + } + + $url += [ + 'plugin' => $params['plugin'], + 'controller' => $params['controller'], + 'action' => 'index', + '_ext' => null + ]; + } + + // If a full URL is requested with a scheme the host should default + // to App.fullBaseUrl to avoid corrupt URLs + if ($full && isset($url['_scheme']) && !isset($url['_host'])) { + $url['_host'] = parse_url(static::fullBaseUrl(), PHP_URL_HOST); + } + + $output = static::$_collection->match($url, static::$_requestContext + ['params' => $params]); + } else { + $plainString = ( + strpos($url, 'javascript:') === 0 || + strpos($url, 'mailto:') === 0 || + strpos($url, 'tel:') === 0 || + strpos($url, 'sms:') === 0 || + strpos($url, '#') === 0 || + strpos($url, '?') === 0 || + strpos($url, '//') === 0 || + strpos($url, '://') !== false + ); + + if ($plainString) { + return $url; + } + $output = $base . $url; + } + $protocol = preg_match('#^[a-z][a-z0-9+\-.]*\://#i', $output); + if ($protocol === 0) { + $output = str_replace('//', '/', '/' . $output); + if ($full) { + $output = static::fullBaseUrl() . $output; + } + } + + return $output . $frag; + } + + /** + * Finds URL for specified action. + * + * Returns a bool if the url exists + * + * ### Usage + * + * @see Router::url() + * + * @param string|array|null $url An array specifying any of the following: + * 'controller', 'action', 'plugin' additionally, you can provide routed + * elements or query string parameters. If string it can be name any valid url + * string. + * @param bool $full If true, the full base URL will be prepended to the result. + * Default is false. + * @return bool + */ + public static function routeExists($url = null, $full = false) + { + try { + $route = static::url($url, $full); + + return true; + } catch (MissingRouteException $e) { + return false; + } + } + + /** + * Sets the full base URL that will be used as a prefix for generating + * fully qualified URLs for this application. If no parameters are passed, + * the currently configured value is returned. + * + * ### Note: + * + * If you change the configuration value `App.fullBaseUrl` during runtime + * and expect the router to produce links using the new setting, you are + * required to call this method passing such value again. + * + * @param string|null $base the prefix for URLs generated containing the domain. + * For example: `http://example.com` + * @return string + */ + public static function fullBaseUrl($base = null) + { + if ($base !== null) { + static::$_fullBaseUrl = $base; + Configure::write('App.fullBaseUrl', $base); + } + if (empty(static::$_fullBaseUrl)) { + static::$_fullBaseUrl = Configure::read('App.fullBaseUrl'); + } + + return static::$_fullBaseUrl; + } + + /** + * Reverses a parsed parameter array into an array. + * + * Works similarly to Router::url(), but since parsed URL's contain additional + * 'pass' as well as 'url.url' keys. Those keys need to be specially + * handled in order to reverse a params array into a string URL. + * + * This will strip out 'autoRender', 'bare', 'requested', and 'return' param names as those + * are used for CakePHP internals and should not normally be part of an output URL. + * + * @param \Cake\Http\ServerRequest|array $params The params array or + * Cake\Http\ServerRequest object that needs to be reversed. + * @return array The URL array ready to be used for redirect or HTML link. + */ + public static function reverseToArray($params) + { + $url = []; + if ($params instanceof ServerRequest) { + $url = $params->getQueryParams(); + $params = $params->getAttribute('params'); + } elseif (isset($params['url'])) { + $url = $params['url']; + } + $pass = isset($params['pass']) ? $params['pass'] : []; + + unset( + $params['pass'], + $params['paging'], + $params['models'], + $params['url'], + $url['url'], + $params['autoRender'], + $params['bare'], + $params['requested'], + $params['return'], + $params['_Token'], + $params['_matchedRoute'], + $params['_name'] + ); + $params = array_merge($params, $pass); + if (!empty($url)) { + $params['?'] = $url; + } + + return $params; + } + + /** + * Reverses a parsed parameter array into a string. + * + * Works similarly to Router::url(), but since parsed URL's contain additional + * 'pass' as well as 'url.url' keys. Those keys need to be specially + * handled in order to reverse a params array into a string URL. + * + * This will strip out 'autoRender', 'bare', 'requested', and 'return' param names as those + * are used for CakePHP internals and should not normally be part of an output URL. + * + * @param \Cake\Http\ServerRequest|array $params The params array or + * Cake\Http\ServerRequest object that needs to be reversed. + * @param bool $full Set to true to include the full URL including the + * protocol when reversing the URL. + * @return string The string that is the reversed result of the array + */ + public static function reverse($params, $full = false) + { + $params = static::reverseToArray($params); + + return static::url($params, $full); + } + + /** + * Normalizes a URL for purposes of comparison. + * + * Will strip the base path off and replace any double /'s. + * It will not unify the casing and underscoring of the input value. + * + * @param array|string $url URL to normalize Either an array or a string URL. + * @return string Normalized URL + */ + public static function normalize($url = '/') + { + if (is_array($url)) { + $url = static::url($url); + } + if (preg_match('/^[a-z\-]+:\/\//', $url)) { + return $url; + } + $request = static::getRequest(); + + if ($request) { + $base = $request->getAttribute('base'); + if (strlen($base) && stristr($url, $base)) { + $url = preg_replace('/^' . preg_quote($base, '/') . '/', '', $url, 1); + } + } + $url = '/' . $url; + + while (strpos($url, '//') !== false) { + $url = str_replace('//', '/', $url); + } + $url = preg_replace('/(?:(\/$))/', '', $url); + + if (empty($url)) { + return '/'; + } + + return $url; + } + + /** + * Get or set valid extensions for all routes connected later. + * + * Instructs the router to parse out file extensions + * from the URL. For example, http://example.com/posts.rss would yield a file + * extension of "rss". The file extension itself is made available in the + * controller as `$this->request->getParam('_ext')`, and is used by the RequestHandler + * component to automatically switch to alternate layouts and templates, and + * load helpers corresponding to the given content, i.e. RssHelper. Switching + * layouts and helpers requires that the chosen extension has a defined mime type + * in `Cake\Http\Response`. + * + * A string or an array of valid extensions can be passed to this method. + * If called without any parameters it will return current list of set extensions. + * + * @param array|string|null $extensions List of extensions to be added. + * @param bool $merge Whether to merge with or override existing extensions. + * Defaults to `true`. + * @return array Array of extensions Router is configured to parse. + */ + public static function extensions($extensions = null, $merge = true) + { + $collection = static::$_collection; + if ($extensions === null) { + if (!static::$initialized) { + static::_loadRoutes(); + } + + return array_unique(array_merge(static::$_defaultExtensions, $collection->getExtensions())); + } + $extensions = (array)$extensions; + if ($merge) { + $extensions = array_unique(array_merge(static::$_defaultExtensions, $extensions)); + } + + return static::$_defaultExtensions = $extensions; + } + + /** + * Provides legacy support for named parameters on incoming URLs. + * + * Checks the passed parameters for elements containing `$options['separator']` + * Those parameters are split and parsed as if they were old style named parameters. + * + * The parsed parameters will be moved from params['pass'] to params['named']. + * + * ### Options + * + * - `separator` The string to use as a separator. Defaults to `:`. + * + * @param \Cake\Http\ServerRequest $request The request object to modify. + * @param array $options The array of options. + * @return \Cake\Http\ServerRequest The modified request + * @deprecated 3.3.0 Named parameter backwards compatibility will be removed in 4.0. + */ + public static function parseNamedParams(ServerRequest $request, array $options = []) + { + deprecationWarning( + 'Router::parseNamedParams() is deprecated. ' . + '2.x backwards compatible named parameter support will be removed in 4.0' + ); + $options += ['separator' => ':']; + if (!$request->getParam('pass')) { + return $request->withParam('named', []); + } + $named = []; + $pass = $request->getParam('pass'); + foreach ((array)$pass as $key => $value) { + if (strpos($value, $options['separator']) === false) { + continue; + } + unset($pass[$key]); + list($key, $value) = explode($options['separator'], $value, 2); + + if (preg_match_all('/\[([A-Za-z0-9_-]+)?\]/', $key, $matches, PREG_SET_ORDER)) { + $matches = array_reverse($matches); + $parts = explode('[', $key); + $key = array_shift($parts); + $arr = $value; + foreach ($matches as $match) { + if (empty($match[1])) { + $arr = [$arr]; + } else { + $arr = [ + $match[1] => $arr + ]; + } + } + $value = $arr; + } + $named = array_merge_recursive($named, [$key => $value]); + } + + return $request + ->withParam('pass', $pass) + ->withParam('named', $named); + } + + /** + * Create a RouteBuilder for the provided path. + * + * @param string $path The path to set the builder to. + * @param array $options The options for the builder + * @return \Cake\Routing\RouteBuilder + */ + public static function createRouteBuilder($path, array $options = []) + { + $defaults = [ + 'routeClass' => static::defaultRouteClass(), + 'extensions' => static::$_defaultExtensions, + ]; + $options += $defaults; + + return new RouteBuilder(static::$_collection, $path, [], [ + 'routeClass' => $options['routeClass'], + 'extensions' => $options['extensions'], + ]); + } + + /** + * Create a routing scope. + * + * Routing scopes allow you to keep your routes DRY and avoid repeating + * common path prefixes, and or parameter sets. + * + * Scoped collections will be indexed by path for faster route parsing. If you + * re-open or re-use a scope the connected routes will be merged with the + * existing ones. + * + * ### Options + * + * The `$params` array allows you to define options for the routing scope. + * The options listed below *are not* available to be used as routing defaults + * + * - `routeClass` The route class to use in this scope. Defaults to + * `Router::defaultRouteClass()` + * - `extensions` The extensions to enable in this scope. Defaults to the globally + * enabled extensions set with `Router::extensions()` + * + * ### Example + * + * ``` + * Router::scope('/blog', ['plugin' => 'Blog'], function ($routes) { + * $routes->connect('/', ['controller' => 'Articles']); + * }); + * ``` + * + * The above would result in a `/blog/` route being created, with both the + * plugin & controller default parameters set. + * + * You can use `Router::plugin()` and `Router::prefix()` as shortcuts to creating + * specific kinds of scopes. + * + * @param string $path The path prefix for the scope. This path will be prepended + * to all routes connected in the scoped collection. + * @param array|callable $params An array of routing defaults to add to each connected route. + * If you have no parameters, this argument can be a callable. + * @param callable|null $callback The callback to invoke with the scoped collection. + * @throws \InvalidArgumentException When an invalid callable is provided. + * @return void + */ + public static function scope($path, $params = [], $callback = null) + { + $options = []; + if (is_array($params)) { + $options = $params; + unset($params['routeClass'], $params['extensions']); + } + $builder = static::createRouteBuilder('/', $options); + $builder->scope($path, $params, $callback); + } + + /** + * Create prefixed routes. + * + * This method creates a scoped route collection that includes + * relevant prefix information. + * + * The path parameter is used to generate the routing parameter name. + * For example a path of `admin` would result in `'prefix' => 'admin'` being + * applied to all connected routes. + * + * The prefix name will be inflected to the underscore version to create + * the routing path. If you want a custom path name, use the `path` option. + * + * You can re-open a prefix as many times as necessary, as well as nest prefixes. + * Nested prefixes will result in prefix values like `admin/api` which translates + * to the `Controller\Admin\Api\` namespace. + * + * @param string $name The prefix name to use. + * @param array|callable $params An array of routing defaults to add to each connected route. + * If you have no parameters, this argument can be a callable. + * @param callable|null $callback The callback to invoke that builds the prefixed routes. + * @return void + */ + public static function prefix($name, $params = [], $callback = null) + { + if ($callback === null) { + $callback = $params; + $params = []; + } + $name = Inflector::underscore($name); + + if (empty($params['path'])) { + $path = '/' . $name; + } else { + $path = $params['path']; + unset($params['path']); + } + + $params = array_merge($params, ['prefix' => $name]); + static::scope($path, $params, $callback); + } + + /** + * Add plugin routes. + * + * This method creates a scoped route collection that includes + * relevant plugin information. + * + * The plugin name will be inflected to the underscore version to create + * the routing path. If you want a custom path name, use the `path` option. + * + * Routes connected in the scoped collection will have the correct path segment + * prepended, and have a matching plugin routing key set. + * + * @param string $name The plugin name to build routes for + * @param array|callable $options Either the options to use, or a callback + * @param callable|null $callback The callback to invoke that builds the plugin routes. + * Only required when $options is defined + * @return void + */ + public static function plugin($name, $options = [], $callback = null) + { + if ($callback === null) { + $callback = $options; + $options = []; + } + $params = ['plugin' => $name]; + if (empty($options['path'])) { + $options['path'] = '/' . Inflector::underscore($name); + } + if (isset($options['_namePrefix'])) { + $params['_namePrefix'] = $options['_namePrefix']; + } + static::scope($options['path'], $params, $callback); + } + + /** + * Get the route scopes and their connected routes. + * + * @return \Cake\Routing\Route\Route[] + */ + public static function routes() + { + if (!static::$initialized) { + static::_loadRoutes(); + } + + return static::$_collection->routes(); + } + + /** + * Get the RouteCollection inside the Router + * + * @return \Cake\Routing\RouteCollection + */ + public static function getRouteCollection() + { + return static::$_collection; + } + + /** + * Set the RouteCollection inside the Router + * + * @param RouteCollection $routeCollection route collection + * @return void + */ + public static function setRouteCollection($routeCollection) + { + static::$_collection = $routeCollection; + static::$initialized = true; + } + + /** + * Loads route configuration + * + * @deprecated 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0 + * @return void + */ + protected static function _loadRoutes() + { + static::$initialized = true; + include CONFIG . 'routes.php'; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/CacheShell.php b/app/vendor/cakephp/cakephp/src/Shell/CacheShell.php new file mode 100644 index 000000000..8a13fee58 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/CacheShell.php @@ -0,0 +1,118 @@ +addSubcommand('list_prefixes', [ + 'help' => 'Show a list of all defined cache prefixes.', + ]); + $parser->addSubcommand('clear_all', [ + 'help' => 'Clear all caches.', + ]); + $parser->addSubcommand('clear', [ + 'help' => 'Clear the cache for a specified prefix.', + 'parser' => [ + 'description' => [ + 'Clear the cache for a particular prefix.', + 'For example, `cake cache clear _cake_model_` will clear the model cache', + 'Use `cake cache list_prefixes` to list available prefixes' + ], + 'arguments' => [ + 'prefix' => [ + 'help' => 'The cache prefix to be cleared.', + 'required' => true + ] + ] + ] + ]); + + return $parser; + } + + /** + * Clear metadata. + * + * @param string|null $prefix The cache prefix to be cleared. + * @throws \Cake\Console\Exception\StopException + * @return void + */ + public function clear($prefix = null) + { + try { + $engine = Cache::engine($prefix); + Cache::clear(false, $prefix); + if ($engine instanceof ApcuEngine) { + $this->warn("ApcuEngine detected: Cleared $prefix CLI cache successfully " . + "but $prefix web cache must be cleared separately."); + } elseif ($engine instanceof WincacheEngine) { + $this->warn("WincacheEngine detected: Cleared $prefix CLI cache successfully " . + "but $prefix web cache must be cleared separately."); + } else { + $this->out("Cleared $prefix cache"); + } + } catch (InvalidArgumentException $e) { + $this->abort($e->getMessage()); + } + } + + /** + * Clear metadata. + * + * @return void + */ + public function clearAll() + { + $prefixes = Cache::configured(); + foreach ($prefixes as $prefix) { + $this->clear($prefix); + } + } + + /** + * Show a list of all defined cache prefixes. + * + * @return void + */ + public function listPrefixes() + { + $prefixes = Cache::configured(); + foreach ($prefixes as $prefix) { + $this->out($prefix); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/CommandListShell.php b/app/vendor/cakephp/cakephp/src/Shell/CommandListShell.php new file mode 100644 index 000000000..44baa9b61 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/CommandListShell.php @@ -0,0 +1,171 @@ +out(); + $this->out(sprintf('Welcome to CakePHP %s Console', 'v' . Configure::version())); + $this->hr(); + $this->out(sprintf('App : %s', APP_DIR)); + $this->out(sprintf('Path: %s', APP)); + $this->out(sprintf('PHP : %s', PHP_VERSION)); + $this->hr(); + } + + /** + * startup + * + * @return void + */ + public function startup() + { + if (!$this->param('xml') && !$this->param('version')) { + parent::startup(); + } + } + + /** + * Main function Prints out the list of shells. + * + * @return void + */ + public function main() + { + if (!$this->param('xml') && !$this->param('version')) { + $this->out('Current Paths:', 2); + $this->out('* app: ' . APP_DIR); + $this->out('* root: ' . rtrim(ROOT, DIRECTORY_SEPARATOR)); + $this->out('* core: ' . rtrim(CORE_PATH, DIRECTORY_SEPARATOR)); + $this->out(''); + + $this->out('Available Shells:', 2); + } + + if ($this->param('version')) { + $this->out(Configure::version()); + + return; + } + + $shellList = $this->Command->getShellList(); + if (!$shellList) { + return; + } + + if (!$this->param('xml')) { + $this->_asText($shellList); + } else { + $this->_asXml($shellList); + } + } + + /** + * Output text. + * + * @param array $shellList The shell list. + * @return void + */ + protected function _asText($shellList) + { + foreach ($shellList as $plugin => $commands) { + sort($commands); + $this->out(sprintf('[%s] %s', $plugin, implode(', ', $commands))); + $this->out(); + } + + $this->out('To run an app or core command, type `cake shell_name [args]`'); + $this->out('To run a plugin command, type `cake Plugin.shell_name [args]`'); + $this->out('To get help on a specific command, type `cake shell_name --help`', 2); + } + + /** + * Output as XML + * + * @param array $shellList The shell list. + * @return void + */ + protected function _asXml($shellList) + { + $plugins = Plugin::loaded(); + $shells = new SimpleXMLElement(''); + foreach ($shellList as $plugin => $commands) { + foreach ($commands as $command) { + $callable = $command; + if (in_array($plugin, $plugins)) { + $callable = Inflector::camelize($plugin) . '.' . $command; + } + + $shell = $shells->addChild('shell'); + $shell->addAttribute('name', $command); + $shell->addAttribute('call_as', $callable); + $shell->addAttribute('provider', $plugin); + $shell->addAttribute('help', $callable . ' -h'); + } + } + $this->_io->setOutputAs(ConsoleOutput::RAW); + $this->out($shells->saveXML()); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->setDescription( + 'Get the list of available shells for this CakePHP application.' + )->addOption('xml', [ + 'help' => 'Get the listing as XML.', + 'boolean' => true + ])->addOption('version', [ + 'help' => 'Prints the currently installed version of CakePHP. (deprecated - use `cake --version` instead)', + 'boolean' => true + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/CompletionShell.php b/app/vendor/cakephp/cakephp/src/Shell/CompletionShell.php new file mode 100644 index 000000000..860c70460 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/CompletionShell.php @@ -0,0 +1,174 @@ +out($this->getOptionParser()->help()); + } + + /** + * list commands + * + * @return int|bool|null Returns the number of bytes returned from writing to stdout. + */ + public function commands() + { + $options = $this->Command->commands(); + + return $this->_output($options); + } + + /** + * list options for the named command + * + * @return int|bool|null Returns the number of bytes returned from writing to stdout. + */ + public function options() + { + $commandName = $subCommandName = ''; + if (!empty($this->args[0])) { + $commandName = $this->args[0]; + } + if (!empty($this->args[1])) { + $subCommandName = $this->args[1]; + } + $options = $this->Command->options($commandName, $subCommandName); + + return $this->_output($options); + } + + /** + * list subcommands for the named command + * + * @return int|bool|null Returns the number of bytes returned from writing to stdout. + * @throws \ReflectionException + */ + public function subcommands() + { + if (!$this->args) { + return $this->_output(); + } + + $options = $this->Command->subCommands($this->args[0]); + + return $this->_output($options); + } + + /** + * Guess autocomplete from the whole argument string + * + * @return int|bool|null Returns the number of bytes returned from writing to stdout. + */ + public function fuzzy() + { + return $this->_output(); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->setDescription( + 'Used by shells like bash to autocomplete command name, options and arguments' + )->addSubcommand('commands', [ + 'help' => 'Output a list of available commands', + 'parser' => [ + 'description' => 'List all available', + ] + ])->addSubcommand('subcommands', [ + 'help' => 'Output a list of available subcommands', + 'parser' => [ + 'description' => 'List subcommands for a command', + 'arguments' => [ + 'command' => [ + 'help' => 'The command name', + 'required' => false, + ] + ] + ] + ])->addSubcommand('options', [ + 'help' => 'Output a list of available options', + 'parser' => [ + 'description' => 'List options', + 'arguments' => [ + 'command' => [ + 'help' => 'The command name', + 'required' => false, + ], + 'subcommand' => [ + 'help' => 'The subcommand name', + 'required' => false, + ] + ] + ] + ])->addSubcommand('fuzzy', [ + 'help' => 'Guess autocomplete' + ])->setEpilog([ + 'This command is not intended to be called manually', + ]); + + return $parser; + } + + /** + * Emit results as a string, space delimited + * + * @param array $options The options to output + * @return int|bool|null Returns the number of bytes returned from writing to stdout. + */ + protected function _output($options = []) + { + if ($options) { + return $this->out(implode($options, ' ')); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/Helper/ProgressHelper.php b/app/vendor/cakephp/cakephp/src/Shell/Helper/ProgressHelper.php new file mode 100644 index 000000000..9d92e5424 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/Helper/ProgressHelper.php @@ -0,0 +1,151 @@ +helper('Progress')->output(['callback' => function ($progress) { + * // Do work + * $progress->increment(); + * }); + * ``` + */ +class ProgressHelper extends Helper +{ + + /** + * The current progress. + * + * @var int + */ + protected $_progress = 0; + + /** + * The total number of 'items' to progress through. + * + * @var int + */ + protected $_total = 0; + + /** + * The width of the bar. + * + * @var int + */ + protected $_width = 0; + + /** + * Output a progress bar. + * + * Takes a number of options to customize the behavior: + * + * - `total` The total number of items in the progress bar. Defaults + * to 100. + * - `width` The width of the progress bar. Defaults to 80. + * - `callback` The callback that will be called in a loop to advance the progress bar. + * + * @param array $args The arguments/options to use when outputing the progress bar. + * @return void + */ + public function output($args) + { + $args += ['callback' => null]; + if (isset($args[0])) { + $args['callback'] = $args[0]; + } + if (!$args['callback'] || !is_callable($args['callback'])) { + throw new RuntimeException('Callback option must be a callable.'); + } + $this->init($args); + + $callback = $args['callback']; + + $this->_io->out('', 0); + while ($this->_progress < $this->_total) { + $callback($this); + $this->draw(); + } + $this->_io->out(''); + } + + /** + * Initialize the progress bar for use. + * + * - `total` The total number of items in the progress bar. Defaults + * to 100. + * - `width` The width of the progress bar. Defaults to 80. + * + * @param array $args The initialization data. + * @return $this + */ + public function init(array $args = []) + { + $args += ['total' => 100, 'width' => 80]; + $this->_progress = 0; + $this->_width = $args['width']; + $this->_total = $args['total']; + + return $this; + } + + /** + * Increment the progress bar. + * + * @param int $num The amount of progress to advance by. + * @return $this + */ + public function increment($num = 1) + { + $this->_progress = min(max(0, $this->_progress + $num), $this->_total); + + return $this; + } + + /** + * Render the progress bar based on the current state. + * + * @return $this + */ + public function draw() + { + $numberLen = strlen(' 100%'); + $complete = round($this->_progress / $this->_total, 2); + $barLen = ($this->_width - $numberLen) * ($this->_progress / $this->_total); + $bar = ''; + if ($barLen > 1) { + $bar = str_repeat('=', $barLen - 1) . '>'; + } + + $pad = ceil($this->_width - $numberLen - $barLen); + if ($pad > 0) { + $bar .= str_repeat(' ', $pad); + } + $percent = ($complete * 100) . '%'; + $bar .= str_pad($percent, $numberLen, ' ', STR_PAD_LEFT); + + $this->_io->overwrite($bar, 0); + + return $this; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/Helper/TableHelper.php b/app/vendor/cakephp/cakephp/src/Shell/Helper/TableHelper.php new file mode 100644 index 000000000..60132ae08 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/Helper/TableHelper.php @@ -0,0 +1,149 @@ + true, + 'rowSeparator' => false, + 'headerStyle' => 'info', + ]; + + /** + * Calculate the column widths + * + * @param array $rows The rows on which the columns width will be calculated on. + * @return array + */ + protected function _calculateWidths($rows) + { + $widths = []; + foreach ($rows as $line) { + foreach (array_values($line) as $k => $v) { + $columnLength = mb_strwidth($v); + if ($columnLength >= (isset($widths[$k]) ? $widths[$k] : 0)) { + $widths[$k] = $columnLength; + } + } + } + + return $widths; + } + + /** + * Output a row separator. + * + * @param array $widths The widths of each column to output. + * @return void + */ + protected function _rowSeparator($widths) + { + $out = ''; + foreach ($widths as $column) { + $out .= '+' . str_repeat('-', $column + 2); + } + $out .= '+'; + $this->_io->out($out); + } + + /** + * Output a row. + * + * @param array $row The row to output. + * @param array $widths The widths of each column to output. + * @param array $options Options to be passed. + * @return void + */ + protected function _render(array $row, $widths, $options = []) + { + if (count($row) === 0) { + return; + } + + $out = ''; + foreach (array_values($row) as $i => $column) { + $pad = $widths[$i] - mb_strwidth($column); + if (!empty($options['style'])) { + $column = $this->_addStyle($column, $options['style']); + } + $out .= '| ' . $column . str_repeat(' ', $pad) . ' '; + } + $out .= '|'; + $this->_io->out($out); + } + + /** + * Output a table. + * + * Data will be output based on the order of the values + * in the array. The keys will not be used to align data. + * + * @param array $rows The data to render out. + * @return void + */ + public function output($rows) + { + if (!is_array($rows) || count($rows) === 0) { + return; + } + + $config = $this->getConfig(); + $widths = $this->_calculateWidths($rows); + + $this->_rowSeparator($widths); + if ($config['headers'] === true) { + $this->_render(array_shift($rows), $widths, ['style' => $config['headerStyle']]); + $this->_rowSeparator($widths); + } + + if (!$rows) { + return; + } + + foreach ($rows as $line) { + $this->_render($line, $widths); + if ($config['rowSeparator'] === true) { + $this->_rowSeparator($widths); + } + } + if ($config['rowSeparator'] !== true) { + $this->_rowSeparator($widths); + } + } + + /** + * Add style tags + * + * @param string $text The text to be surrounded + * @param string $style The style to be applied + * @return string + */ + protected function _addStyle($text, $style) + { + return '<' . $style . '>' . $text . ''; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/I18nShell.php b/app/vendor/cakephp/cakephp/src/Shell/I18nShell.php new file mode 100644 index 000000000..40828f227 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/I18nShell.php @@ -0,0 +1,168 @@ +out('I18n Shell'); + $this->hr(); + $this->out('[E]xtract POT file from sources'); + $this->out('[I]nitialize a language from POT file'); + $this->out('[H]elp'); + $this->out('[Q]uit'); + + $choice = strtolower($this->in('What would you like to do?', ['E', 'I', 'H', 'Q'])); + switch ($choice) { + case 'e': + $this->Extract->main(); + break; + case 'i': + $this->init(); + break; + case 'h': + $this->out($this->OptionParser->help()); + break; + case 'q': + $this->_stop(); + + return; + default: + $this->out('You have made an invalid selection. Please choose a command to execute by entering E, I, H, or Q.'); + } + $this->hr(); + $this->main(); + } + + /** + * Inits PO file from POT file. + * + * @param string|null $language Language code to use. + * @return void + * @throws \Cake\Console\Exception\StopException + */ + public function init($language = null) + { + if (!$language) { + $language = $this->in('Please specify language code, e.g. `en`, `eng`, `en_US` etc.'); + } + if (strlen($language) < 2) { + $this->abort('Invalid language code. Valid is `en`, `eng`, `en_US` etc.'); + } + + $this->_paths = [APP]; + if ($this->param('plugin')) { + $plugin = Inflector::camelize($this->param('plugin')); + $this->_paths = [Plugin::classPath($plugin)]; + } + + $response = $this->in('What folder?', null, rtrim($this->_paths[0], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'Locale'); + $sourceFolder = rtrim($response, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + $targetFolder = $sourceFolder . $language . DIRECTORY_SEPARATOR; + if (!is_dir($targetFolder)) { + mkdir($targetFolder, 0775, true); + } + + $count = 0; + $iterator = new DirectoryIterator($sourceFolder); + foreach ($iterator as $fileinfo) { + if (!$fileinfo->isFile()) { + continue; + } + $filename = $fileinfo->getFilename(); + $newFilename = $fileinfo->getBasename('.pot'); + $newFilename .= '.po'; + + $this->createFile($targetFolder . $newFilename, file_get_contents($sourceFolder . $filename)); + $count++; + } + + $this->out('Generated ' . $count . ' PO files in ' . $targetFolder); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + * @throws \Cake\Console\Exception\ConsoleException + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + $initParser = [ + 'options' => [ + 'plugin' => [ + 'help' => 'Plugin name.', + 'short' => 'p' + ], + 'force' => [ + 'help' => 'Force overwriting.', + 'short' => 'f', + 'boolean' => true + ] + ], + 'arguments' => [ + 'language' => [ + 'help' => 'Two-letter language code.' + ] + ] + ]; + + $parser->setDescription( + 'I18n Shell generates .pot files(s) with translations.' + )->addSubcommand('extract', [ + 'help' => 'Extract the po translations from your application', + 'parser' => $this->Extract->getOptionParser() + ]) + ->addSubcommand('init', [ + 'help' => 'Init PO language file from POT file', + 'parser' => $initParser + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/OrmCacheShell.php b/app/vendor/cakephp/cakephp/src/Shell/OrmCacheShell.php new file mode 100644 index 000000000..7d4448258 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/OrmCacheShell.php @@ -0,0 +1,31 @@ +out($loaded); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->setDescription('Plugin Shell perform various tasks related to plugin.') + ->addSubcommand('assets', [ + 'help' => 'Symlink / copy plugin assets to app\'s webroot', + 'parser' => $this->Assets->getOptionParser() + ]) + ->addSubcommand('loaded', [ + 'help' => 'Lists all loaded plugins', + 'parser' => $parser, + ]) + ->addSubcommand('load', [ + 'help' => 'Loads a plugin', + 'parser' => $this->Load->getOptionParser(), + ]) + ->addSubcommand('unload', [ + 'help' => 'Unloads a plugin', + 'parser' => $this->Unload->getOptionParser(), + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/RoutesShell.php b/app/vendor/cakephp/cakephp/src/Shell/RoutesShell.php new file mode 100644 index 000000000..00eb608fc --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/RoutesShell.php @@ -0,0 +1,156 @@ +options['_name']) ? $route->options['_name'] : $route->getName(); + ksort($route->defaults); + $output[] = [$name, $route->template, json_encode($route->defaults)]; + } + $this->helper('table')->output($output); + $this->out(); + } + + /** + * Checks a url for the route that will be applied. + * + * @param string $url The URL to parse + * @return bool Success + */ + public function check($url) + { + try { + $request = new ServerRequest(['url' => $url]); + $route = Router::parseRequest($request); + $name = null; + foreach (Router::routes() as $r) { + if ($r->match($route)) { + $name = isset($r->options['_name']) ? $r->options['_name'] : $r->getName(); + break; + } + } + + unset($route['_matchedRoute']); + ksort($route); + + $output = [ + ['Route name', 'URI template', 'Defaults'], + [$name, $url, json_encode($route)] + ]; + $this->helper('table')->output($output); + $this->out(); + } catch (MissingRouteException $e) { + $this->warn("'$url' did not match any routes."); + $this->out(); + + return false; + } + + return true; + } + + /** + * Generate a URL based on a set of parameters + * + * Takes variadic arguments of key/value pairs. + * @return bool Success + */ + public function generate() + { + try { + $args = $this->_splitArgs($this->args); + $url = Router::url($args); + $this->out("> $url"); + $this->out(); + } catch (MissingRouteException $e) { + $this->err('The provided parameters do not match any routes.'); + $this->out(); + + return false; + } + + return true; + } + + /** + * Get the option parser. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + $parser->setDescription( + 'Get the list of routes connected in this application. ' . + 'This tool also lets you test URL generation and URL parsing.' + )->addSubcommand('check', [ + 'help' => 'Check a URL string against the routes. ' . + 'Will output the routing parameters the route resolves to.' + ])->addSubcommand('generate', [ + 'help' => 'Check a routing array against the routes. ' . + "Will output the URL if there is a match.\n\n" . + 'Routing parameters should be supplied in a key:value format. ' . + 'For example `controller:Articles action:view 2`' + ]); + + return $parser; + } + + /** + * Split the CLI arguments into a hash. + * + * @param array $args The arguments to split. + * @return array + */ + protected function _splitArgs($args) + { + $out = []; + foreach ($args as $arg) { + if (strpos($arg, ':') !== false) { + list($key, $value) = explode(':', $arg); + if (in_array($value, ['true', 'false'])) { + $value = $value === 'true'; + } + $out[$key] = $value; + } else { + $out[] = $arg; + } + } + + return $out; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/SchemaCacheShell.php b/app/vendor/cakephp/cakephp/src/Shell/SchemaCacheShell.php new file mode 100644 index 000000000..61eea2fe0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/SchemaCacheShell.php @@ -0,0 +1,115 @@ +_getSchemaCache(); + $tables = $cache->build($name); + + foreach ($tables as $table) { + $this->verbose(sprintf('Cached "%s"', $table)); + } + + $this->out('Cache build complete'); + + return true; + } + + /** + * Clear metadata. + * + * @param string|null $name The name of the table to clear cache data for. + * @return bool + */ + public function clear($name = null) + { + $cache = $this->_getSchemaCache(); + $tables = $cache->clear($name); + + foreach ($tables as $table) { + $this->verbose(sprintf('Cleared "%s"', $table)); + } + + $this->out('Cache clear complete'); + + return true; + } + + /** + * Gets the Schema Cache instance + * + * @return \Cake\Database\SchemaCache + */ + protected function _getSchemaCache() + { + try { + $connection = ConnectionManager::get($this->params['connection']); + + return new SchemaCache($connection); + } catch (RuntimeException $e) { + $this->abort($e->getMessage()); + } + } + + /** + * Get the option parser for this shell. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + $parser->addSubcommand('clear', [ + 'help' => 'Clear all metadata caches for the connection. If a ' . + 'table name is provided, only that table will be removed.', + ])->addSubcommand('build', [ + 'help' => 'Build all metadata caches for the connection. If a ' . + 'table name is provided, only that table will be cached.', + ])->addOption('connection', [ + 'help' => 'The connection to build/clear metadata cache data for.', + 'short' => 'c', + 'default' => 'default', + ])->addArgument('name', [ + 'help' => 'A specific table you want to clear/refresh cached data for.', + 'optional' => true, + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/ServerShell.php b/app/vendor/cakephp/cakephp/src/Shell/ServerShell.php new file mode 100644 index 000000000..6bcdea17e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/ServerShell.php @@ -0,0 +1,181 @@ +param('host')) { + $this->_host = $this->param('host'); + } + if ($this->param('port')) { + $this->_port = $this->param('port'); + } + if ($this->param('document_root')) { + $this->_documentRoot = $this->param('document_root'); + } + if ($this->param('ini_path')) { + $this->_iniPath = $this->param('ini_path'); + } + + // For Windows + if (substr($this->_documentRoot, -1, 1) === DIRECTORY_SEPARATOR) { + $this->_documentRoot = substr($this->_documentRoot, 0, strlen($this->_documentRoot) - 1); + } + if (preg_match("/^([a-z]:)[\\\]+(.+)$/i", $this->_documentRoot, $m)) { + $this->_documentRoot = $m[1] . '\\' . $m[2]; + } + + $this->_iniPath = rtrim($this->_iniPath, DIRECTORY_SEPARATOR); + if (preg_match("/^([a-z]:)[\\\]+(.+)$/i", $this->_iniPath, $m)) { + $this->_iniPath = $m[1] . '\\' . $m[2]; + } + + parent::startup(); + } + + /** + * Displays a header for the shell + * + * @return void + */ + protected function _welcome() + { + $this->out(); + $this->out(sprintf('Welcome to CakePHP %s Console', 'v' . Configure::version())); + $this->hr(); + $this->out(sprintf('App : %s', APP_DIR)); + $this->out(sprintf('Path: %s', APP)); + $this->out(sprintf('DocumentRoot: %s', $this->_documentRoot)); + $this->out(sprintf('Ini Path: %s', $this->_iniPath)); + $this->hr(); + } + + /** + * Override main() to handle action + * + * @return void + */ + public function main() + { + $command = sprintf( + 'php -S %s:%d -t %s', + $this->_host, + $this->_port, + escapeshellarg($this->_documentRoot) + ); + + if (!empty($this->_iniPath)) { + $command = sprintf('%s -c %s', $command, $this->_iniPath); + } + + $command = sprintf('%s %s', $command, escapeshellarg($this->_documentRoot . '/index.php')); + + $port = ':' . $this->_port; + $this->out(sprintf('built-in server is running in http://%s%s/', $this->_host, $port)); + $this->out(sprintf('You can exit with `CTRL-C`')); + system($command); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->setDescription([ + 'PHP Built-in Server for CakePHP', + '[WARN] Don\'t use this in a production environment', + ])->addOption('host', [ + 'short' => 'H', + 'help' => 'ServerHost' + ])->addOption('port', [ + 'short' => 'p', + 'help' => 'ListenPort' + ])->addOption('ini_path', [ + 'short' => 'I', + 'help' => 'php.ini path' + ])->addOption('document_root', [ + 'short' => 'd', + 'help' => 'DocumentRoot' + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/Task/AssetsTask.php b/app/vendor/cakephp/cakephp/src/Shell/Task/AssetsTask.php new file mode 100644 index 000000000..de193e533 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/Task/AssetsTask.php @@ -0,0 +1,339 @@ +_process($this->_list($name)); + } + + /** + * Copying plugin assets to app's webroot. For vendor namespaced plugin, + * parent folder for vendor name are created if required. + * + * @param string|null $name Name of plugin for which to symlink assets. + * If null all plugins will be processed. + * @return void + */ + public function copy($name = null) + { + $this->_process($this->_list($name), true, $this->param('overwrite')); + } + + /** + * Remove plugin assets from app's webroot. + * + * @param string|null $name Name of plugin for which to remove assets. + * If null all plugins will be processed. + * @return void + * @since 3.5.12 + */ + public function remove($name = null) + { + $plugins = $this->_list($name); + + foreach ($plugins as $plugin => $config) { + $this->out(); + $this->out('For plugin: ' . $plugin); + $this->hr(); + + $this->_remove($config); + } + + $this->out(); + $this->out('Done'); + } + + /** + * Get list of plugins to process. Plugins without a webroot directory are skipped. + * + * @param string|null $name Name of plugin for which to symlink assets. + * If null all plugins will be processed. + * @return array List of plugins with meta data. + */ + protected function _list($name = null) + { + if ($name === null) { + $pluginsList = Plugin::loaded(); + } else { + if (!Plugin::loaded($name)) { + $this->err(sprintf('Plugin %s is not loaded.', $name)); + + return []; + } + $pluginsList = [$name]; + } + + $plugins = []; + + foreach ($pluginsList as $plugin) { + $path = Plugin::path($plugin) . 'webroot'; + if (!is_dir($path)) { + $this->verbose('', 1); + $this->verbose( + sprintf('Skipping plugin %s. It does not have webroot folder.', $plugin), + 2 + ); + continue; + } + + $link = Inflector::underscore($plugin); + $dir = WWW_ROOT; + $namespaced = false; + if (strpos($link, '/') !== false) { + $namespaced = true; + $parts = explode('/', $link); + $link = array_pop($parts); + $dir = WWW_ROOT . implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR; + } + + $plugins[$plugin] = [ + 'srcPath' => Plugin::path($plugin) . 'webroot', + 'destDir' => $dir, + 'link' => $link, + 'namespaced' => $namespaced + ]; + } + + return $plugins; + } + + /** + * Process plugins + * + * @param array $plugins List of plugins to process + * @param bool $copy Force copy mode. Default false. + * @param bool $overwrite Overwrite existing files. + * @return void + */ + protected function _process($plugins, $copy = false, $overwrite = false) + { + $overwrite = (bool)$this->param('overwrite'); + + foreach ($plugins as $plugin => $config) { + $this->out(); + $this->out('For plugin: ' . $plugin); + $this->hr(); + + if ($config['namespaced'] && + !is_dir($config['destDir']) && + !$this->_createDirectory($config['destDir']) + ) { + continue; + } + + $dest = $config['destDir'] . $config['link']; + + if (file_exists($dest)) { + if ($overwrite && !$this->_remove($config)) { + continue; + } elseif (!$overwrite) { + $this->verbose( + $dest . ' already exists', + 1 + ); + + continue; + } + } + + if (!$copy) { + $result = $this->_createSymlink( + $config['srcPath'], + $dest + ); + if ($result) { + continue; + } + } + + $this->_copyDirectory( + $config['srcPath'], + $dest + ); + } + + $this->out(); + $this->out('Done'); + } + + /** + * Remove folder/symlink. + * + * @param array $config Plugin config. + * @return bool + */ + protected function _remove($config) + { + if ($config['namespaced'] && !is_dir($config['destDir'])) { + $this->verbose( + $config['destDir'] . $config['link'] . ' does not exist', + 1 + ); + + return false; + } + + $dest = $config['destDir'] . $config['link']; + + if (!file_exists($dest)) { + $this->verbose( + $dest . ' does not exist', + 1 + ); + + return false; + } + + if (is_link($dest)) { + // @codingStandardsIgnoreLine + if (@unlink($dest)) { + $this->out('Unlinked ' . $dest); + + return true; + } else { + $this->err('Failed to unlink ' . $dest); + + return false; + } + } + + $folder = new Folder($dest); + if ($folder->delete()) { + $this->out('Deleted ' . $dest); + + return true; + } else { + $this->err('Failed to delete ' . $dest); + + return false; + } + } + + /** + * Create directory + * + * @param string $dir Directory name + * @return bool + */ + protected function _createDirectory($dir) + { + $old = umask(0); + // @codingStandardsIgnoreStart + $result = @mkdir($dir, 0755, true); + // @codingStandardsIgnoreEnd + umask($old); + + if ($result) { + $this->out('Created directory ' . $dir); + + return true; + } + + $this->err('Failed creating directory ' . $dir); + + return false; + } + + /** + * Create symlink + * + * @param string $target Target directory + * @param string $link Link name + * @return bool + */ + protected function _createSymlink($target, $link) + { + // @codingStandardsIgnoreStart + $result = @symlink($target, $link); + // @codingStandardsIgnoreEnd + + if ($result) { + $this->out('Created symlink ' . $link); + + return true; + } + + return false; + } + + /** + * Copy directory + * + * @param string $source Source directory + * @param string $destination Destination directory + * @return bool + */ + protected function _copyDirectory($source, $destination) + { + $folder = new Folder($source); + if ($folder->copy(['to' => $destination])) { + $this->out('Copied assets to directory ' . $destination); + + return true; + } + + $this->err('Error copying assets to directory ' . $destination); + + return false; + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->addSubcommand('symlink', [ + 'help' => 'Symlink (copy as fallback) plugin assets to app\'s webroot.' + ])->addSubcommand('copy', [ + 'help' => 'Copy plugin assets to app\'s webroot.' + ])->addSubcommand('remove', [ + 'help' => 'Remove plugin assets from app\'s webroot.' + ])->addArgument('name', [ + 'help' => 'A specific plugin you want to symlink assets for.', + 'optional' => true, + ])->addOption('overwrite', [ + 'help' => 'Overwrite existing symlink / folder / files.', + 'default' => false, + 'boolean' => true + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/Task/CommandTask.php b/app/vendor/cakephp/cakephp/src/Shell/Task/CommandTask.php new file mode 100644 index 000000000..12874ac07 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/Task/CommandTask.php @@ -0,0 +1,289 @@ + null, 'app' => null]; + + $appPath = App::path('Shell'); + $shellList = $this->_findShells($shellList, $appPath[0], 'app', $skipFiles); + + $appPath = App::path('Command'); + $shellList = $this->_findShells($shellList, $appPath[0], 'app', $skipFiles); + + $skipCore = array_merge($skipFiles, $hiddenCommands, $shellList['app']); + $corePath = dirname(__DIR__); + $shellList = $this->_findShells($shellList, $corePath, 'CORE', $skipCore); + + $corePath = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'Command'; + $shellList = $this->_findShells($shellList, $corePath, 'CORE', $skipCore); + + foreach ($plugins as $plugin) { + $pluginPath = Plugin::classPath($plugin) . 'Shell'; + $shellList = $this->_findShells($shellList, $pluginPath, $plugin, []); + } + + return array_filter($shellList); + } + + /** + * Find shells in $path and add them to $shellList + * + * @param array $shellList The shell listing array. + * @param string $path The path to look in. + * @param string $key The key to add shells to + * @param array $skip A list of commands to exclude. + * @return array The updated list of shells. + */ + protected function _findShells($shellList, $path, $key, $skip) + { + $shells = $this->_scanDir($path); + + return $this->_appendShells($key, $shells, $shellList, $skip); + } + + /** + * Scan the provided paths for shells, and append them into $shellList + * + * @param string $type The type of object. + * @param array $shells The shell name. + * @param array $shellList List of shells. + * @param array $skip List of command names to skip. + * @return array The updated $shellList + */ + protected function _appendShells($type, $shells, $shellList, $skip) + { + if (!isset($shellList[$type])) { + $shellList[$type] = []; + } + + foreach ($shells as $shell) { + $name = Inflector::underscore(preg_replace('/(Shell|Command)$/', '', $shell)); + if (!in_array($name, $skip, true)) { + $shellList[$type][] = $name; + } + } + sort($shellList[$type]); + + return $shellList; + } + + /** + * Scan a directory for .php files and return the class names that + * should be within them. + * + * @param string $dir The directory to read. + * @return array The list of shell classnames based on conventions. + */ + protected function _scanDir($dir) + { + $dir = new Folder($dir); + $contents = $dir->read(true, true); + if (empty($contents[1])) { + return []; + } + $shells = []; + foreach ($contents[1] as $file) { + if (substr($file, -4) !== '.php') { + continue; + } + $shells[] = substr($file, 0, -4); + } + + return $shells; + } + + /** + * Return a list of all commands + * + * @return array + */ + public function commands() + { + $shellList = $this->getShellList(); + $flatten = Hash::flatten($shellList); + $duplicates = array_intersect($flatten, array_unique(array_diff_key($flatten, array_unique($flatten)))); + $duplicates = Hash::expand($duplicates); + + $options = []; + foreach ($shellList as $type => $commands) { + foreach ($commands as $shell) { + $prefix = ''; + if (!in_array(strtolower($type), ['app', 'core']) && + isset($duplicates[$type]) && + in_array($shell, $duplicates[$type]) + ) { + $prefix = $type . '.'; + } + + $options[] = $prefix . $shell; + } + } + + return $options; + } + + /** + * Return a list of subcommands for a given command + * + * @param string $commandName The command you want subcommands from. + * @return array + * @throws \ReflectionException + */ + public function subCommands($commandName) + { + $Shell = $this->getShell($commandName); + + if (!$Shell) { + return []; + } + + $taskMap = $this->Tasks->normalizeArray((array)$Shell->tasks); + $return = array_keys($taskMap); + $return = array_map('Cake\Utility\Inflector::underscore', $return); + + $shellMethodNames = ['main', 'help', 'getOptionParser', 'initialize', 'runCommand']; + + $baseClasses = ['Object', 'Shell', 'AppShell']; + + $Reflection = new ReflectionClass($Shell); + $methods = $Reflection->getMethods(ReflectionMethod::IS_PUBLIC); + $methodNames = []; + foreach ($methods as $method) { + $declaringClass = $method->getDeclaringClass()->getShortName(); + if (!in_array($declaringClass, $baseClasses)) { + $methodNames[] = $method->getName(); + } + } + + $return = array_merge($return, array_diff($methodNames, $shellMethodNames)); + sort($return); + + return $return; + } + + /** + * Get Shell instance for the given command + * + * @param string $commandName The command you want. + * @return \Cake\Console\Shell|bool Shell instance if the command can be found, false otherwise. + */ + public function getShell($commandName) + { + list($pluginDot, $name) = pluginSplit($commandName, true); + + if (in_array(strtolower($pluginDot), ['app.', 'core.'])) { + $commandName = $name; + $pluginDot = ''; + } + + if (!in_array($commandName, $this->commands()) && (empty($pluginDot) && !in_array($name, $this->commands()))) { + return false; + } + + if (empty($pluginDot)) { + $shellList = $this->getShellList(); + + if (!in_array($commandName, $shellList['app']) && !in_array($commandName, $shellList['CORE'])) { + unset($shellList['CORE'], $shellList['app']); + foreach ($shellList as $plugin => $commands) { + if (in_array($commandName, $commands)) { + $pluginDot = $plugin . '.'; + break; + } + } + } + } + + $name = Inflector::camelize($name); + $pluginDot = Inflector::camelize($pluginDot); + $class = App::className($pluginDot . $name, 'Shell', 'Shell'); + if (!$class) { + return false; + } + + /* @var \Cake\Console\Shell $Shell */ + $Shell = new $class(); + $Shell->plugin = trim($pluginDot, '.'); + $Shell->initialize(); + + return $Shell; + } + + /** + * Get options list for the given command or subcommand + * + * @param string $commandName The command to get options for. + * @param string $subCommandName The subcommand to get options for. Can be empty to get options for the command. + * If this parameter is used, the subcommand must be a valid subcommand of the command passed + * @return array Options list for the given command or subcommand + */ + public function options($commandName, $subCommandName = '') + { + $Shell = $this->getShell($commandName); + + if (!$Shell) { + return []; + } + + $parser = $Shell->getOptionParser(); + + if (!empty($subCommandName)) { + $subCommandName = Inflector::camelize($subCommandName); + if ($Shell->hasTask($subCommandName)) { + $parser = $Shell->{$subCommandName}->getOptionParser(); + } else { + return []; + } + } + + $options = []; + $array = $parser->options(); + /* @var \Cake\Console\ConsoleInputOption $obj */ + foreach ($array as $name => $obj) { + $options[] = "--$name"; + $short = $obj->short(); + if ($short) { + $options[] = "-$short"; + } + } + + return $options; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/Task/ExtractTask.php b/app/vendor/cakephp/cakephp/src/Shell/Task/ExtractTask.php new file mode 100644 index 000000000..19127158c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/Task/ExtractTask.php @@ -0,0 +1,756 @@ +_paths) > 0 ? $this->_paths : ['None']; + $message = sprintf( + "Current paths: %s\nWhat is the path you would like to extract?\n[Q]uit [D]one", + implode(', ', $currentPaths) + ); + $response = $this->in($message, null, $defaultPath); + if (strtoupper($response) === 'Q') { + $this->err('Extract Aborted'); + $this->_stop(); + + return; + } + if (strtoupper($response) === 'D' && count($this->_paths)) { + $this->out(); + + return; + } + if (strtoupper($response) === 'D') { + $this->warn('No directories selected. Please choose a directory.'); + } elseif (is_dir($response)) { + $this->_paths[] = $response; + $defaultPath = 'D'; + } else { + $this->err('The directory path you supplied was not found. Please try again.'); + } + $this->out(); + } + } + + /** + * Execution method always used for tasks + * + * @return void + */ + public function main() + { + if (!empty($this->params['exclude'])) { + $this->_exclude = explode(',', $this->params['exclude']); + } + if (isset($this->params['files']) && !is_array($this->params['files'])) { + $this->_files = explode(',', $this->params['files']); + } + if (isset($this->params['paths'])) { + $this->_paths = explode(',', $this->params['paths']); + } elseif (isset($this->params['plugin'])) { + $plugin = Inflector::camelize($this->params['plugin']); + if (!Plugin::loaded($plugin)) { + Plugin::load($plugin); + } + $this->_paths = [Plugin::classPath($plugin)]; + $this->params['plugin'] = $plugin; + } else { + $this->_getPaths(); + } + + if (isset($this->params['extract-core'])) { + $this->_extractCore = !(strtolower($this->params['extract-core']) === 'no'); + } else { + $response = $this->in('Would you like to extract the messages from the CakePHP core?', ['y', 'n'], 'n'); + $this->_extractCore = strtolower($response) === 'y'; + } + + if (!empty($this->params['exclude-plugins']) && $this->_isExtractingApp()) { + $this->_exclude = array_merge($this->_exclude, App::path('Plugin')); + } + + if (!empty($this->params['validation-domain'])) { + $this->_validationDomain = $this->params['validation-domain']; + } + + if ($this->_extractCore) { + $this->_paths[] = CAKE; + } + + if (isset($this->params['output'])) { + $this->_output = $this->params['output']; + } elseif (isset($this->params['plugin'])) { + $this->_output = $this->_paths[0] . 'Locale'; + } else { + $message = "What is the path you would like to output?\n[Q]uit"; + while (true) { + $response = $this->in($message, null, rtrim($this->_paths[0], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'Locale'); + if (strtoupper($response) === 'Q') { + $this->err('Extract Aborted'); + $this->_stop(); + + return; + } + if ($this->_isPathUsable($response)) { + $this->_output = $response . DIRECTORY_SEPARATOR; + break; + } + + $this->err(''); + $this->err( + 'The directory path you supplied was ' . + 'not found. Please try again.' + ); + $this->out(); + } + } + + if (isset($this->params['merge'])) { + $this->_merge = !(strtolower($this->params['merge']) === 'no'); + } else { + $this->out(); + $response = $this->in('Would you like to merge all domain strings into the default.pot file?', ['y', 'n'], 'n'); + $this->_merge = strtolower($response) === 'y'; + } + + if (empty($this->_files)) { + $this->_searchFiles(); + } + + $this->_output = rtrim($this->_output, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + if (!$this->_isPathUsable($this->_output)) { + $this->err(sprintf('The output directory %s was not found or writable.', $this->_output)); + $this->_stop(); + + return; + } + + $this->_extract(); + } + + /** + * Add a translation to the internal translations property + * + * Takes care of duplicate translations + * + * @param string $domain The domain + * @param string $msgid The message string + * @param array $details Context and plural form if any, file and line references + * @return void + */ + protected function _addTranslation($domain, $msgid, $details = []) + { + $context = isset($details['msgctxt']) ? $details['msgctxt'] : ''; + + if (empty($this->_translations[$domain][$msgid][$context])) { + $this->_translations[$domain][$msgid][$context] = [ + 'msgid_plural' => false + ]; + } + + if (isset($details['msgid_plural'])) { + $this->_translations[$domain][$msgid][$context]['msgid_plural'] = $details['msgid_plural']; + } + + if (isset($details['file'])) { + $line = isset($details['line']) ? $details['line'] : 0; + $this->_translations[$domain][$msgid][$context]['references'][$details['file']][] = $line; + } + } + + /** + * Extract text + * + * @return void + */ + protected function _extract() + { + $this->out(); + $this->out(); + $this->out('Extracting...'); + $this->hr(); + $this->out('Paths:'); + foreach ($this->_paths as $path) { + $this->out(' ' . $path); + } + $this->out('Output Directory: ' . $this->_output); + $this->hr(); + $this->_extractTokens(); + $this->_buildFiles(); + $this->_writeFiles(); + $this->_paths = $this->_files = $this->_storage = []; + $this->_translations = $this->_tokens = []; + $this->out(); + $this->out('Done.'); + } + + /** + * Gets the option parser instance and configures it. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + $parser->setDescription( + 'CakePHP Language String Extraction:' + )->addOption('app', [ + 'help' => 'Directory where your application is located.' + ])->addOption('paths', [ + 'help' => 'Comma separated list of paths.' + ])->addOption('merge', [ + 'help' => 'Merge all domain strings into the default.po file.', + 'choices' => ['yes', 'no'] + ])->addOption('output', [ + 'help' => 'Full path to output directory.' + ])->addOption('files', [ + 'help' => 'Comma separated list of files.' + ])->addOption('exclude-plugins', [ + 'boolean' => true, + 'default' => true, + 'help' => 'Ignores all files in plugins if this command is run inside from the same app directory.' + ])->addOption('plugin', [ + 'help' => 'Extracts tokens only from the plugin specified and puts the result in the plugin\'s Locale directory.' + ])->addOption('ignore-model-validation', [ + 'boolean' => true, + 'default' => false, + 'help' => 'Ignores validation messages in the $validate property.' . + ' If this flag is not set and the command is run from the same app directory,' . + ' all messages in model validation rules will be extracted as tokens.' + ])->addOption('validation-domain', [ + 'help' => 'If set to a value, the localization domain to be used for model validation messages.' + ])->addOption('exclude', [ + 'help' => 'Comma separated list of directories to exclude.' . + ' Any path containing a path segment with the provided values will be skipped. E.g. test,vendors' + ])->addOption('overwrite', [ + 'boolean' => true, + 'default' => false, + 'help' => 'Always overwrite existing .pot files.' + ])->addOption('extract-core', [ + 'help' => 'Extract messages from the CakePHP core libs.', + 'choices' => ['yes', 'no'] + ])->addOption('no-location', [ + 'boolean' => true, + 'default' => false, + 'help' => 'Do not write file locations for each extracted message.', + ]); + + return $parser; + } + + /** + * Extract tokens out of all files to be processed + * + * @return void + */ + protected function _extractTokens() + { + /** @var \Cake\Shell\Helper\ProgressHelper $progress */ + $progress = $this->helper('progress'); + $progress->init(['total' => count($this->_files)]); + $isVerbose = $this->param('verbose'); + + foreach ($this->_files as $file) { + $this->_file = $file; + if ($isVerbose) { + $this->out(sprintf('Processing %s...', $file), 1, Shell::VERBOSE); + } + + $code = file_get_contents($file); + $allTokens = token_get_all($code); + + $this->_tokens = []; + foreach ($allTokens as $token) { + if (!is_array($token) || ($token[0] !== T_WHITESPACE && $token[0] !== T_INLINE_HTML)) { + $this->_tokens[] = $token; + } + } + unset($allTokens); + $this->_parse('__', ['singular']); + $this->_parse('__n', ['singular', 'plural']); + $this->_parse('__d', ['domain', 'singular']); + $this->_parse('__dn', ['domain', 'singular', 'plural']); + $this->_parse('__x', ['context', 'singular']); + $this->_parse('__xn', ['context', 'singular', 'plural']); + $this->_parse('__dx', ['domain', 'context', 'singular']); + $this->_parse('__dxn', ['domain', 'context', 'singular', 'plural']); + + if (!$isVerbose) { + $progress->increment(1); + $progress->draw(); + } + } + } + + /** + * Parse tokens + * + * @param string $functionName Function name that indicates translatable string (e.g: '__') + * @param array $map Array containing what variables it will find (e.g: domain, singular, plural) + * @return void + */ + protected function _parse($functionName, $map) + { + $count = 0; + $tokenCount = count($this->_tokens); + + while (($tokenCount - $count) > 1) { + $countToken = $this->_tokens[$count]; + $firstParenthesis = $this->_tokens[$count + 1]; + if (!is_array($countToken)) { + $count++; + continue; + } + + list($type, $string, $line) = $countToken; + if (($type == T_STRING) && ($string === $functionName) && ($firstParenthesis === '(')) { + $position = $count; + $depth = 0; + + while (!$depth) { + if ($this->_tokens[$position] === '(') { + $depth++; + } elseif ($this->_tokens[$position] === ')') { + $depth--; + } + $position++; + } + + $mapCount = count($map); + $strings = $this->_getStrings($position, $mapCount); + + if ($mapCount === count($strings)) { + $singular = null; + extract(array_combine($map, $strings)); + $domain = isset($domain) ? $domain : 'default'; + $details = [ + 'file' => $this->_file, + 'line' => $line, + ]; + if (isset($plural)) { + $details['msgid_plural'] = $plural; + } + if (isset($context)) { + $details['msgctxt'] = $context; + } + $this->_addTranslation($domain, $singular, $details); + } elseif (strpos($this->_file, CAKE_CORE_INCLUDE_PATH) === false) { + $this->_markerError($this->_file, $line, $functionName, $count); + } + } + $count++; + } + } + + /** + * Build the translate template file contents out of obtained strings + * + * @return void + */ + protected function _buildFiles() + { + $paths = $this->_paths; + $paths[] = realpath(APP) . DIRECTORY_SEPARATOR; + + usort($paths, function ($a, $b) { + return strlen($a) - strlen($b); + }); + + foreach ($this->_translations as $domain => $translations) { + foreach ($translations as $msgid => $contexts) { + foreach ($contexts as $context => $details) { + $plural = $details['msgid_plural']; + $files = $details['references']; + $occurrences = []; + foreach ($files as $file => $lines) { + $lines = array_unique($lines); + $occurrences[] = $file . ':' . implode(';', $lines); + } + $occurrences = implode("\n#: ", $occurrences); + $header = ''; + if (!$this->param('no-location')) { + $header = '#: ' . str_replace(DIRECTORY_SEPARATOR, '/', str_replace($paths, '', $occurrences)) . "\n"; + } + + $sentence = ''; + if ($context !== '') { + $sentence .= "msgctxt \"{$context}\"\n"; + } + if ($plural === false) { + $sentence .= "msgid \"{$msgid}\"\n"; + $sentence .= "msgstr \"\"\n\n"; + } else { + $sentence .= "msgid \"{$msgid}\"\n"; + $sentence .= "msgid_plural \"{$plural}\"\n"; + $sentence .= "msgstr[0] \"\"\n"; + $sentence .= "msgstr[1] \"\"\n\n"; + } + + if ($domain !== 'default' && $this->_merge) { + $this->_store('default', $header, $sentence); + } else { + $this->_store($domain, $header, $sentence); + } + } + } + } + } + + /** + * Prepare a file to be stored + * + * @param string $domain The domain + * @param string $header The header content. + * @param string $sentence The sentence to store. + * @return void + */ + protected function _store($domain, $header, $sentence) + { + if (!isset($this->_storage[$domain])) { + $this->_storage[$domain] = []; + } + if (!isset($this->_storage[$domain][$sentence])) { + $this->_storage[$domain][$sentence] = $header; + } else { + $this->_storage[$domain][$sentence] .= $header; + } + } + + /** + * Write the files that need to be stored + * + * @return void + */ + protected function _writeFiles() + { + $overwriteAll = false; + if (!empty($this->params['overwrite'])) { + $overwriteAll = true; + } + foreach ($this->_storage as $domain => $sentences) { + $output = $this->_writeHeader(); + foreach ($sentences as $sentence => $header) { + $output .= $header . $sentence; + } + + // Remove vendor prefix if present. + $slashPosition = strpos($domain, '/'); + if ($slashPosition !== false) { + $domain = substr($domain, $slashPosition + 1); + } + + $filename = str_replace('/', '_', $domain) . '.pot'; + $File = new File($this->_output . $filename); + $response = ''; + while ($overwriteAll === false && $File->exists() && strtoupper($response) !== 'Y') { + $this->out(); + $response = $this->in( + sprintf('Error: %s already exists in this location. Overwrite? [Y]es, [N]o, [A]ll', $filename), + ['y', 'n', 'a'], + 'y' + ); + if (strtoupper($response) === 'N') { + $response = ''; + while (!$response) { + $response = $this->in('What would you like to name this file?', null, 'new_' . $filename); + $File = new File($this->_output . $response); + $filename = $response; + } + } elseif (strtoupper($response) === 'A') { + $overwriteAll = true; + } + } + $File->write($output); + $File->close(); + } + } + + /** + * Build the translation template header + * + * @return string Translation template header + */ + protected function _writeHeader() + { + $output = "# LANGUAGE translation of CakePHP Application\n"; + $output .= "# Copyright YEAR NAME \n"; + $output .= "#\n"; + $output .= "#, fuzzy\n"; + $output .= "msgid \"\"\n"; + $output .= "msgstr \"\"\n"; + $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; + $output .= '"POT-Creation-Date: ' . date('Y-m-d H:iO') . "\\n\"\n"; + $output .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n"; + $output .= "\"Last-Translator: NAME \\n\"\n"; + $output .= "\"Language-Team: LANGUAGE \\n\"\n"; + $output .= "\"MIME-Version: 1.0\\n\"\n"; + $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; + $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; + $output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n"; + + return $output; + } + + /** + * Get the strings from the position forward + * + * @param int $position Actual position on tokens array + * @param int $target Number of strings to extract + * @return array Strings extracted + */ + protected function _getStrings(&$position, $target) + { + $strings = []; + $count = count($strings); + while ($count < $target && ($this->_tokens[$position] === ',' || $this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position][0] == T_LNUMBER)) { + $count = count($strings); + if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING && $this->_tokens[$position + 1] === '.') { + $string = ''; + while ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position] === '.') { + if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) { + $string .= $this->_formatString($this->_tokens[$position][1]); + } + $position++; + } + $strings[] = $string; + } elseif ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) { + $strings[] = $this->_formatString($this->_tokens[$position][1]); + } elseif ($this->_tokens[$position][0] == T_LNUMBER) { + $strings[] = $this->_tokens[$position][1]; + } + $position++; + } + + return $strings; + } + + /** + * Format a string to be added as a translatable string + * + * @param string $string String to format + * @return string Formatted string + */ + protected function _formatString($string) + { + $quote = substr($string, 0, 1); + $string = substr($string, 1, -1); + if ($quote === '"') { + $string = stripcslashes($string); + } else { + $string = strtr($string, ["\\'" => "'", '\\\\' => '\\']); + } + $string = str_replace("\r\n", "\n", $string); + + return addcslashes($string, "\0..\37\\\""); + } + + /** + * Indicate an invalid marker on a processed file + * + * @param string $file File where invalid marker resides + * @param int $line Line number + * @param string $marker Marker found + * @param int $count Count + * @return void + */ + protected function _markerError($file, $line, $marker, $count) + { + $this->err(sprintf("Invalid marker content in %s:%s\n* %s(", $file, $line, $marker)); + $count += 2; + $tokenCount = count($this->_tokens); + $parenthesis = 1; + + while ((($tokenCount - $count) > 0) && $parenthesis) { + if (is_array($this->_tokens[$count])) { + $this->err($this->_tokens[$count][1], false); + } else { + $this->err($this->_tokens[$count], false); + if ($this->_tokens[$count] === '(') { + $parenthesis++; + } + + if ($this->_tokens[$count] === ')') { + $parenthesis--; + } + } + $count++; + } + $this->err("\n", true); + } + + /** + * Search files that may contain translatable strings + * + * @return void + */ + protected function _searchFiles() + { + $pattern = false; + if (!empty($this->_exclude)) { + $exclude = []; + foreach ($this->_exclude as $e) { + if (DIRECTORY_SEPARATOR !== '\\' && $e[0] !== DIRECTORY_SEPARATOR) { + $e = DIRECTORY_SEPARATOR . $e; + } + $exclude[] = preg_quote($e, '/'); + } + $pattern = '/' . implode('|', $exclude) . '/'; + } + foreach ($this->_paths as $path) { + $path = realpath($path) . DIRECTORY_SEPARATOR; + $Folder = new Folder($path); + $files = $Folder->findRecursive('.*\.(php|ctp|thtml|inc|tpl)', true); + if (!empty($pattern)) { + $files = preg_grep($pattern, $files, PREG_GREP_INVERT); + $files = array_values($files); + } + $this->_files = array_merge($this->_files, $files); + } + $this->_files = array_unique($this->_files); + } + + /** + * Returns whether this execution is meant to extract string only from directories in folder represented by the + * APP constant, i.e. this task is extracting strings from same application. + * + * @return bool + */ + protected function _isExtractingApp() + { + return $this->_paths === [APP]; + } + + /** + * Checks whether or not a given path is usable for writing. + * + * @param string $path Path to folder + * @return bool true if it exists and is writable, false otherwise + */ + protected function _isPathUsable($path) + { + if (!is_dir($path)) { + mkdir($path, 0770, true); + } + + return is_dir($path) && is_writable($path); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/Task/LoadTask.php b/app/vendor/cakephp/cakephp/src/Shell/Task/LoadTask.php new file mode 100644 index 000000000..8fc005dd2 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/Task/LoadTask.php @@ -0,0 +1,175 @@ +params['cli']) { + $filename .= '_cli'; + } + + $this->bootstrap = ROOT . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . $filename . '.php'; + + if (!$plugin) { + $this->err('You must provide a plugin name in CamelCase format.'); + $this->err('To load an "Example" plugin, run `cake plugin load Example`.'); + + return false; + } + + $options = $this->makeOptions(); + + $app = APP . 'Application.php'; + if (file_exists($app) && !$this->param('no_app')) { + $this->modifyApplication($app, $plugin, $options); + + return true; + } + + return $this->_modifyBootstrap($plugin, $options); + } + + /** + * Create options string for the load call. + * + * @return string + */ + protected function makeOptions() + { + $autoloadString = $this->param('autoload') ? "'autoload' => true" : ''; + $bootstrapString = $this->param('bootstrap') ? "'bootstrap' => true" : ''; + $routesString = $this->param('routes') ? "'routes' => true" : ''; + + return implode(', ', array_filter([$autoloadString, $bootstrapString, $routesString])); + } + + /** + * Modify the application class + * + * @param string $app The Application file to modify. + * @param string $plugin The plugin name to add. + * @param string $options The plugin options to add + * @return void + */ + protected function modifyApplication($app, $plugin, $options) + { + $file = new File($app, false); + $contents = $file->read(); + + $append = "\n \$this->addPlugin('%s', [%s]);\n"; + $insert = str_replace(', []', '', sprintf($append, $plugin, $options)); + + if (!preg_match('/function bootstrap\(\)/m', $contents)) { + $this->abort('Your Application class does not have a bootstrap() method. Please add one.'); + } else { + $contents = preg_replace('/(function bootstrap\(\)(?:\s+)\{)/m', '$1' . $insert, $contents); + } + $file->write($contents); + + $this->out(''); + $this->out(sprintf('%s modified', $app)); + } + + /** + * Update the applications bootstrap.php file. + * + * @param string $plugin Name of plugin. + * @param string $options The options string + * @return bool If modify passed. + */ + protected function _modifyBootstrap($plugin, $options) + { + $bootstrap = new File($this->bootstrap, false); + $contents = $bootstrap->read(); + if (!preg_match("@\n\s*Plugin::loadAll@", $contents)) { + $append = "\nPlugin::load('%s', [%s]);\n"; + + $bootstrap->append(str_replace(', []', '', sprintf($append, $plugin, $options))); + $this->out(''); + $this->out(sprintf('%s modified', $this->bootstrap)); + + return true; + } + + return false; + } + + /** + * GetOptionParser method. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->addOption('bootstrap', [ + 'short' => 'b', + 'help' => 'Will load bootstrap.php from plugin.', + 'boolean' => true, + 'default' => false, + ]) + ->addOption('routes', [ + 'short' => 'r', + 'help' => 'Will load routes.php from plugin.', + 'boolean' => true, + 'default' => false, + ]) + ->addOption('autoload', [ + 'help' => 'Will autoload the plugin using CakePHP.' . + 'Set to true if you are not using composer to autoload your plugin.', + 'boolean' => true, + 'default' => false, + ]) + ->addOption('cli', [ + 'help' => 'Use the bootstrap_cli file.', + 'boolean' => true, + 'default' => false, + ]) + ->addOption('no_app', [ + 'help' => 'Do not update the Application if it exist. Forces config/bootstrap.php to be updated.', + 'boolean' => true, + 'default' => false, + ]) + ->addArgument('plugin', [ + 'help' => 'Name of the plugin to load.', + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Shell/Task/UnloadTask.php b/app/vendor/cakephp/cakephp/src/Shell/Task/UnloadTask.php new file mode 100644 index 000000000..6139e7119 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Shell/Task/UnloadTask.php @@ -0,0 +1,147 @@ +params['cli']) { + $filename .= '_cli'; + } + + $this->bootstrap = ROOT . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . $filename . '.php'; + + if (!$plugin) { + $this->err('You must provide a plugin name in CamelCase format.'); + $this->err('To unload an "Example" plugin, run `cake plugin unload Example`.'); + + return false; + } + + $app = APP . 'Application.php'; + if (file_exists($app) && !$this->param('no_app')) { + $this->modifyApplication($app, $plugin); + + return true; + } + + return (bool)$this->_modifyBootstrap($plugin); + } + + /** + * Update the applications bootstrap.php file. + * + * @param string $app Path to the application to update. + * @param string $plugin Name of plugin. + * @return bool If modify passed. + */ + protected function modifyApplication($app, $plugin) + { + $finder = "@\\\$this\-\>addPlugin\(\s*'$plugin'(.|.\n|)+\);+@"; + + $content = file_get_contents($app); + $newContent = preg_replace($finder, '', $content); + + if ($newContent === $content) { + return false; + } + + file_put_contents($app, $newContent); + + $this->out(''); + $this->out(sprintf('%s modified', $app)); + + return true; + } + + /** + * Update the applications bootstrap.php file. + * + * @param string $plugin Name of plugin. + * @return bool If modify passed. + */ + protected function _modifyBootstrap($plugin) + { + $finder = "@\nPlugin::load\((.|.\n|\n\s\s|\n\t|)+'$plugin'(.|.\n|)+\);\n@"; + + $bootstrap = new File($this->bootstrap, false); + $content = $bootstrap->read(); + + if (!preg_match("@\n\s*Plugin::loadAll@", $content)) { + $newContent = preg_replace($finder, '', $content); + + if ($newContent === $content) { + return false; + } + + $bootstrap->write($newContent); + + $this->out(''); + $this->out(sprintf('%s modified', $this->bootstrap)); + + return true; + } + + return false; + } + + /** + * GetOptionParser method. + * + * @return \Cake\Console\ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->addOption('cli', [ + 'help' => 'Use the bootstrap_cli file.', + 'boolean' => true, + 'default' => false, + ]) + ->addOption('no_app', [ + 'help' => 'Do not update the Application if it exist. Forces config/bootstrap.php to be updated.', + 'boolean' => true, + 'default' => false, + ]) + ->addArgument('plugin', [ + 'help' => 'Name of the plugin to load.', + ]); + + return $parser; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Template/Element/auto_table_warning.ctp b/app/vendor/cakephp/cakephp/src/Template/Element/auto_table_warning.ctp new file mode 100644 index 000000000..16628f16c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Element/auto_table_warning.ctp @@ -0,0 +1,42 @@ +genericInstances(); +if (!$autoTables) { + return; +} +?> +

Could this be caused by using Auto-Tables?

+

+Some of the Table objects in your application were created by instantiating "Cake\ORM\Table" +instead of any other specific subclass. +

+

This could be the cause for this exception. Auto-Tables are created for you under the following circumstances:

+
    +
  • The class for the specified table does not exist.
  • +
  • The Table was created with a typo: $this->getTableLocator()->get('Atricles');
  • +
  • The class file has a typo in the name or incorrect namespace: class Atricles extends Table
  • +
  • The file containing the class has a typo or incorrect casing: Atricles.php
  • +
  • The Table was used using associations but the association has a typo: $this->belongsTo('Atricles')
  • +
  • The table class resides in a Plugin but no plugin notation was used in the association definition.
  • +
+
+

Please try correcting the issue for the following table aliases:

+
    + $table) : ?> +
  • + +
+
diff --git a/app/vendor/cakephp/cakephp/src/Template/Element/exception_stack_trace.ctp b/app/vendor/cakephp/cakephp/src/Template/Element/exception_stack_trace.ctp new file mode 100644 index 000000000..5b4bf7c63 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Element/exception_stack_trace.ctp @@ -0,0 +1,62 @@ + + +getTrace() as $i => $stack): + $excerpt = $params = []; + + if (isset($stack['file'], $stack['line'])): + $excerpt = Debugger::excerpt($stack['file'], $stack['line'], 4); + endif; + + if (isset($stack['file'])): + $file = $stack['file']; + else: + $file = '[internal function]'; + endif; + + if ($stack['function']): + if (!empty($stack['args'])): + foreach ((array)$stack['args'] as $arg): + $params[] = Debugger::exportVar($arg, 4); + endforeach; + else: + $params[] = 'No arguments'; + endif; + endif; +?> + + diff --git a/app/vendor/cakephp/cakephp/src/Template/Element/exception_stack_trace_nav.ctp b/app/vendor/cakephp/cakephp/src/Template/Element/exception_stack_trace_nav.ctp new file mode 100644 index 000000000..859c08940 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Element/exception_stack_trace_nav.ctp @@ -0,0 +1,43 @@ + +toggle vendor stack frames + + diff --git a/app/vendor/cakephp/cakephp/src/Template/Element/plugin_class_error.ctp b/app/vendor/cakephp/cakephp/src/Template/Element/plugin_class_error.ctp new file mode 100644 index 000000000..aebe9e831 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Element/plugin_class_error.ctp @@ -0,0 +1,33 @@ +
'; + +if (!Plugin::loaded($plugin)): + echo sprintf('Make sure your plugin %s is in the %s directory and was loaded.', h($plugin), $pluginPath); +else: + echo sprintf('Make sure your plugin was loaded from %s and Composer is able to autoload its classes, see %s and %s', + 'config' . DIRECTORY_SEPARATOR . 'bootstrap.php', + 'Loading a plugin', + 'Plugins - autoloading plugin classes' + ); +endif; + +?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/duplicate_named_route.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/duplicate_named_route.ctp new file mode 100644 index 000000000..d88d5f828 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/duplicate_named_route.ctp @@ -0,0 +1,61 @@ +layout = 'dev_error'; + +$this->assign('title', 'Duplicate Named Route'); +$this->assign('templateName', 'duplicate_named_route.ctp'); + +$attributes = $error->getAttributes(); + +$this->start('subheading'); +?> + Error: + getMessage()); ?> +end() ?> + +start('file') ?> +

Route names must be unique across your entire application. +The same _name option cannot be used twice, +even if the names occur in different routing scopes. +Remove duplicate route names in your route configuration.

+ + +

The passed context was:

+
+
+
+ + + +

Duplicate Route

+ + + '; + printf( + '', + h($other->template), + h(Debugger::exportVar($other->defaults)), + h(Debugger::exportVar($other->options)) + ); + echo ''; + ?> +
TemplateDefaultsOptions
%s%s%s
+ +end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/fatal_error.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/fatal_error.ctp new file mode 100644 index 000000000..22262746a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/fatal_error.ctp @@ -0,0 +1,38 @@ +layout = 'dev_error'; + +$this->assign('title', 'Fatal Error'); +$this->assign('templateName', 'fatal_error.ctp'); + +$this->start('subheading'); +?> + Error: + getMessage()) ?> +
+ + File + getFile()) ?> +
+ Line: + getLine()) ?> +end() ?> + +start('file'); +if (extension_loaded('xdebug')): + xdebug_print_function_stack(); +endif; +$this->end(); diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_action.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_action.ctp new file mode 100644 index 000000000..243e2c3fc --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_action.ctp @@ -0,0 +1,86 @@ +layout = 'dev_error'; + +$this->assign('title', sprintf('Missing Method in %s', h($class))); +$this->assign( + 'subheading', + sprintf('The action %s is not defined in %s', h($action), h($class)) +); +$this->assign('templateName', 'missing_action.ctp'); + +$this->start('file'); +?> +

+ Error: + %s::%s() in file: %s.', h($class), h($action), $path); ?> +

+ + + +
+end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_behavior.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_behavior.ctp new file mode 100644 index 000000000..37c6189df --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_behavior.ctp @@ -0,0 +1,68 @@ +layout = 'dev_error'; + +$this->assign('templateName', 'missing_behavior.ctp'); + +$this->assign('title', 'Missing Behavior'); + +$this->start('subheading'); +printf('%s could not be found.', h($pluginDot . $class)); +echo $this->element('plugin_class_error', ['pluginPath' => $pluginPath]); +$this->end(); + +$this->start('file'); +?> +

+ Error: + %s below in file: %s', h($class), $filePath . 'Model' . DIRECTORY_SEPARATOR . 'Behavior' . DIRECTORY_SEPARATOR . h($class) . '.php'); ?> +

+ + +
+end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_cell_view.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_cell_view.ctp new file mode 100644 index 000000000..ad59435f7 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_cell_view.ctp @@ -0,0 +1,43 @@ +layout = 'dev_error'; + +$this->assign('templateName', 'missing_cell_view.ctp'); +$this->assign('title', 'Missing Cell View'); + +$this->start('subheading'); +printf('The view for %sCell was not be found.', h(Inflector::camelize($name))); +$this->end(); + +$this->start('file'); +?> +

+ Confirm you have created the file: "_ext) ?>" + in one of the following paths: +

+
    +_paths($this->plugin); + foreach ($paths as $path): + if (strpos($path, CORE_PATH) !== false) { + continue; + } + echo sprintf('
  • %sCell/%s/%s
  • ', h($path), h($name), h($file . $this->_ext)); + endforeach; +?> +
+end(); ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_component.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_component.ctp new file mode 100644 index 000000000..d9ff8ef53 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_component.ctp @@ -0,0 +1,64 @@ +layout = 'dev_error'; +$this->assign('title', 'Missing Component'); +$this->assign('templateName', 'missing_component.ctp'); + +$this->start('subheading'); +printf('%s could not be found.', h($pluginDot . $class)); +echo $this->element('plugin_class_error', ['pluginPath' => $pluginPath]); +$this->end(); + +$this->start('file'); +?> +

+ Error: + %s below in file: %s', h($class), $filePath . 'Controller' . DIRECTORY_SEPARATOR . 'Component' . DIRECTORY_SEPARATOR . h($class) . '.php'); ?> +

+ +
+end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_connection.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_connection.ctp new file mode 100644 index 000000000..fe9ed4b9d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_connection.ctp @@ -0,0 +1,32 @@ +layout = 'dev_error'; + +$this->assign('templateName', 'missing_connection.ctp'); +$this->assign('title', 'Missing Database Connection'); + + +$this->start('subheading'); ?> +A Database connection using was missing or unable to connect. +
+end(); + +$this->start('file'); +echo $this->element('auto_table_warning'); +$this->end(); diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_controller.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_controller.ctp new file mode 100644 index 000000000..503b7e470 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_controller.ctp @@ -0,0 +1,91 @@ +layout = 'dev_error'; + +$this->assign('title', 'Missing Controller'); +$this->assign('templateName', 'missing_controller.ctp'); + +?> +start('subheading');?> +Error: + + Your routing resulted in as a controller name. + + Controller could not be found. + +end() ?> + + +start('file'); ?> + +

The controller name has not been properly inflected, and + could not be resolved to a controller that exists in your application.

+ +

Ensure that your URL request->getUri()->getPath()) ?> is + using the same inflection style as your routes do. By default applications use DashedRoute + and URLs should use - to separate multi-word controller names.

+ +

+ In the case you tried to access a plugin controller make sure you added it to your composer file or you use the autoload option for the plugin. +

+

+ Error: + Create the class Controller below in file: +

+ + +
+ +end(); ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_datasource.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_datasource.ctp new file mode 100644 index 000000000..2102d821a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_datasource.ctp @@ -0,0 +1,29 @@ +layout = 'dev_error'; + +$this->assign('title', 'Missing Datasource'); +$this->assign('templateName', 'missing_datasource.ctp'); + +$this->start('subheading'); +?> +Error: +Datasource class could not be found. + + + +end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_datasource_config.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_datasource_config.ctp new file mode 100644 index 000000000..05b0fa153 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_datasource_config.ctp @@ -0,0 +1,29 @@ +layout = 'dev_error'; + +$this->assign('title', 'Missing Datasource Configuration'); +$this->assign('templateName', 'missing_datasource_config.ctp'); + +$this->start('subheading'); +?> + Error: + + The datasource configuration was not found in config. + + + +end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_helper.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_helper.ctp new file mode 100644 index 000000000..1bbecba16 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_helper.ctp @@ -0,0 +1,65 @@ +layout = 'dev_error'; +$this->assign('title', 'Missing Helper'); +$this->assign('templateName', 'missing_helper.ctp'); + +$this->start('subheading'); +?> + Error: + could not be found. + element('plugin_class_error', ['pluginPath' => $pluginPath]) ?> +end() ?> + +start('file') ?> +

+ Error: + %s below in file: %s', h($class), $filePath . 'View' . DIRECTORY_SEPARATOR . 'Helper' . DIRECTORY_SEPARATOR . h($class) . '.php'); ?> +

+ +
+end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_layout.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_layout.ctp new file mode 100644 index 000000000..d53649b3d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_layout.ctp @@ -0,0 +1,42 @@ +layout = 'dev_error'; + +$this->assign('title', 'Missing Layout'); +$this->assign('templateName', 'missing_layout.ctp'); + +$this->start('subheading'); +?> + Error: + The layout file can not be found or does not exist. +end() ?> + +start('file') ?> +

+ Confirm you have created the file: in one of the following paths: +

+
    +_paths($this->plugin); + foreach ($paths as $path): + if (strpos($path, CORE_PATH) !== false) { + continue; + } + echo sprintf('
  • %s%s
  • ', h($path), h($file)); + endforeach; +?> +
+end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_plugin.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_plugin.ctp new file mode 100644 index 000000000..73e4fb121 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_plugin.ctp @@ -0,0 +1,49 @@ +layout = 'dev_error'; + +$pluginPath = Configure::read('App.paths.plugins.0'); + +$this->assign('title', 'Missing Plugin'); +$this->assign('templateName', 'missing_plugin.ctp'); + +$this->start('subheading'); +?> + Error: + The application is trying to load a file from the plugin. +
+
+ Make sure your plugin is in the directory and was loaded. +end() ?> + +start('file') ?> +addPlugin('{$plugin}'); +} +PHP; + +?> +
+ +end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_route.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_route.ctp new file mode 100644 index 000000000..4a248c93e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_route.ctp @@ -0,0 +1,59 @@ +layout = 'dev_error'; + +$this->assign('title', 'Missing Route'); +$this->assign('templateName', 'missing_route.ctp'); + +$attributes = $error->getAttributes(); + +$this->start('subheading'); +?> + Error: + getMessage()); ?> +end() ?> + +start('file') ?> +

None of the currently connected routes match the provided parameters. +Add a matching route to

+ + +

The passed context was:

+
+
+
+ + +

Connected Routes

+ + +'; + printf( + '', + h($route->template), + h(Debugger::exportVar($route->defaults)), + h(Debugger::exportVar($route->options)) + ); + echo ''; +endforeach; +?> +
TemplateDefaultsOptions
%s%s%s
+end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_template.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_template.ctp new file mode 100644 index 000000000..d2529f67c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_template.ctp @@ -0,0 +1,55 @@ +layout = 'dev_error'; + +$this->assign('title', 'Missing Template'); +$this->assign('templateName', 'missing_template.ctp'); + +$isEmail = strpos($file, 'Email/') === 0; + +$this->start('subheading'); +?> + + Error: + was not found.', h($file)); ?> + + Error: + %sController::%s() was not found.', + h(Inflector::camelize($this->request->getParam('controller'))), + h($this->request->getParam('action')) + ); ?> + +end() ?> + +start('file') ?> +

+ + in one of the following paths: +

+
    +_paths($this->plugin); + foreach ($paths as $path): + if (strpos($path, CORE_PATH) !== false) { + continue; + } + echo sprintf('
  • %s%s
  • ', h($path), h($file)); + endforeach; +?> +
+end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/missing_view.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/missing_view.ctp new file mode 100644 index 000000000..99d385fb9 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/missing_view.ctp @@ -0,0 +1,67 @@ +layout = 'dev_error'; +$this->assign('title', 'Missing View'); +$this->assign('templateName', 'missing_view.ctp'); + +$this->start('subheading'); +?> + Error: + could not be found. + + Make sure your plugin is in the directory and was loaded. + + element('plugin_class_error', ['pluginPath' => $pluginPath]) ?> + +end() ?> + +start('file') ?> +

+ Error: + %s below in file: %s', h($class), $filePath . 'View' . DIRECTORY_SEPARATOR . h($class) . '.php'); ?> +

+ +
+end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Error/pdo_error.ctp b/app/vendor/cakephp/cakephp/src/Template/Error/pdo_error.ctp new file mode 100644 index 000000000..154fb2538 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Error/pdo_error.ctp @@ -0,0 +1,44 @@ +setLayout('dev_error'); + +$this->assign('title', 'Database Error'); +$this->assign('templateName', 'pdo_error.ctp'); + +$this->start('subheading'); +?> + Error: + +end() ?> + +start('file') ?> +

+ If you are using SQL keywords as table column names, you can enable identifier + quoting for your database connection in config/app.php. +

+queryString)) : ?> +

+ SQL Query: +

+
queryString); ?>
+ +params)) : ?> + SQL Query Params: +
params)); ?>
+ +element('auto_table_warning'); ?> +end() ?> diff --git a/app/vendor/cakephp/cakephp/src/Template/Layout/dev_error.ctp b/app/vendor/cakephp/cakephp/src/Template/Layout/dev_error.ctp new file mode 100644 index 000000000..c747fd7f0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Template/Layout/dev_error.ctp @@ -0,0 +1,297 @@ + + + + + Html->charset() ?> + + + Error: <?= h($this->fetch('title')) ?> + + Html->meta('icon') ?> + + + +
+

+ fetch('title')) ?> + +

+ +
+ +
+ fetch('subheading')): ?> +

+ fetch('subheading') ?> +

+ + + element('exception_stack_trace'); ?> + +
+ fetch('file') ?> +
+ + fetch('templateName')): ?> +

+ If you want to customize this error message, create + fetch('templateName') ?> +

+ +
+ +
+ element('exception_stack_trace_nav') ?> +
+ + + + diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/ConsoleIntegrationTestCase.php b/app/vendor/cakephp/cakephp/src/TestSuite/ConsoleIntegrationTestCase.php new file mode 100644 index 000000000..b4c1772b8 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/ConsoleIntegrationTestCase.php @@ -0,0 +1,324 @@ +_makeRunner(); + + $this->_out = new ConsoleOutput(); + $this->_err = new ConsoleOutput(); + $this->_in = $this->getMockBuilder(ConsoleInput::class) + ->disableOriginalConstructor() + ->setMethods(['read']) + ->getMock(); + + $i = 0; + foreach ($input as $in) { + $this->_in + ->expects($this->at($i++)) + ->method('read') + ->will($this->returnValue($in)); + } + + $args = $this->_commandStringToArgs("cake $command"); + + $io = new ConsoleIo($this->_out, $this->_err, $this->_in); + + try { + $this->_exitCode = $runner->run($args, $io); + } catch (StopException $exception) { + $this->_exitCode = $exception->getCode(); + } + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + + $this->_exitCode = null; + $this->_out = null; + $this->_err = null; + $this->_in = null; + $this->_useCommandRunner = false; + } + + /** + * Set this test case to use the CommandRunner rather than the legacy + * ShellDispatcher + * + * @return void + */ + public function useCommandRunner() + { + $this->_useCommandRunner = true; + } + + /** + * Asserts shell exited with the expected code + * + * @param int $expected Expected exit code + * @param string $message Failure message to be appended to the generated message + * @return void + */ + public function assertExitCode($expected, $message = '') + { + $message = sprintf( + 'Shell exited with code %d instead of the expected code %d. %s', + $this->_exitCode, + $expected, + $message + ); + $this->assertSame($expected, $this->_exitCode, $message); + } + + /** + * Asserts that `stdout` is empty + * + * @param string $message The message to output when the assertion fails. + * @return void + */ + public function assertOutputEmpty($message = 'stdout was not empty') + { + $output = implode(PHP_EOL, $this->_out->messages()); + $this->assertSame('', $output, $message); + } + + /** + * Asserts `stdout` contains expected output + * + * @param string $expected Expected output + * @param string $message Failure message + * @return void + */ + public function assertOutputContains($expected, $message = '') + { + $output = implode(PHP_EOL, $this->_out->messages()); + $this->assertContains($expected, $output, $message); + } + /** + * Asserts `stdout` does not contain expected output + * + * @param string $expected Expected output + * @param string $message Failure message + * @return void + */ + public function assertOutputNotContains($expected, $message = '') + { + $output = implode(PHP_EOL, $this->_out->messages()); + $this->assertNotContains($expected, $output, $message); + } + + /** + * Asserts `stdout` contains expected regexp + * + * @param string $pattern Expected pattern + * @param string $message Failure message + * @return void + */ + public function assertOutputRegExp($pattern, $message = '') + { + $output = implode(PHP_EOL, $this->_out->messages()); + $this->assertRegExp($pattern, $output, $message); + } + + /** + * Check that a row of cells exists in the output. + * + * @param array $row Row of cells to ensure exist in the output. + * @param string $message Failure message. + * @return void + */ + protected function assertOutputContainsRow(array $row, $message = '') + { + $row = array_map(function ($cell) { + return preg_quote($cell, '/'); + }, $row); + $cells = implode('\s+\|\s+', $row); + $pattern = '/' . $cells . '/'; + $this->assertOutputRegExp($pattern); + } + + /** + * Asserts `stderr` contains expected output + * + * @param string $expected Expected output + * @param string $message Failure message + * @return void + */ + public function assertErrorContains($expected, $message = '') + { + $output = implode(PHP_EOL, $this->_err->messages()); + $this->assertContains($expected, $output, $message); + } + + /** + * Asserts `stderr` contains expected regexp + * + * @param string $pattern Expected pattern + * @param string $message Failure message + * @return void + */ + public function assertErrorRegExp($pattern, $message = '') + { + $output = implode(PHP_EOL, $this->_err->messages()); + $this->assertRegExp($pattern, $output, $message); + } + + /** + * Asserts that `stderr` is empty + * + * @param string $message The message to output when the assertion fails. + * @return void + */ + public function assertErrorEmpty($message = 'stderr was not empty') + { + $output = implode(PHP_EOL, $this->_err->messages()); + $this->assertSame('', $output, $message); + } + + /** + * Builds the appropriate command dispatcher + * + * @return CommandRunner|LegacyCommandRunner + */ + protected function _makeRunner() + { + if ($this->_useCommandRunner) { + $applicationClassName = Configure::read('App.namespace') . '\Application'; + /** @var \Cake\Http\BaseApplication $applicationClass */ + $applicationClass = new $applicationClassName(CONFIG); + + return new CommandRunner($applicationClass); + } + + return new LegacyCommandRunner(); + } + + /** + * Creates an $argv array from a command string + * + * @param string $command Command string + * @return array + */ + protected function _commandStringToArgs($command) + { + $charCount = strlen($command); + $argv = []; + $arg = ''; + $inDQuote = false; + $inSQuote = false; + for ($i = 0; $i < $charCount; $i++) { + $char = substr($command, $i, 1); + + // end of argument + if ($char === ' ' && !$inDQuote && !$inSQuote) { + if (strlen($arg)) { + $argv[] = $arg; + } + $arg = ''; + continue; + } + + // exiting single quote + if ($inSQuote && $char === "'") { + $inSQuote = false; + continue; + } + + // exiting double quote + if ($inDQuote && $char === '"') { + $inDQuote = false; + continue; + } + + // entering double quote + if ($char === '"' && !$inSQuote) { + $inDQuote = true; + continue; + } + + // entering single quote + if ($char === "'" && !$inDQuote) { + $inSQuote = true; + continue; + } + + $arg .= $char; + } + $argv[] = $arg; + + return $argv; + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFired.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFired.php new file mode 100644 index 000000000..c3fe9e177 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFired.php @@ -0,0 +1,65 @@ +_eventManager = $eventManager; + + if ($this->_eventManager->getEventList() === null) { + throw new AssertionFailedError('The event manager you are asserting against is not configured to track events.'); + } + } + + /** + * Checks if event is in fired array + * + * @param mixed $other Constraint check + * @return bool + */ + public function matches($other) + { + return $this->_eventManager->getEventList()->hasEvent($other); + } + + /** + * Assertion message string + * + * @return string + */ + public function toString() + { + return 'was fired'; + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFiredWith.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFiredWith.php new file mode 100644 index 000000000..b28008233 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFiredWith.php @@ -0,0 +1,116 @@ +_eventManager = $eventManager; + $this->_dataKey = $dataKey; + $this->_dataValue = $dataValue; + + if ($this->_eventManager->getEventList() === null) { + throw new AssertionFailedError('The event manager you are asserting against is not configured to track events.'); + } + } + + /** + * Checks if event is in fired array + * + * @param mixed $other Constraint check + * @return bool + */ + public function matches($other) + { + $firedEvents = []; + $list = $this->_eventManager->getEventList(); + $totalEvents = count($list); + for ($e = 0; $e < $totalEvents; $e++) { + $firedEvents[] = $list[$e]; + } + + $eventGroup = collection($firedEvents) + ->groupBy(function (Event $event) { + return $event->getName(); + }) + ->toArray(); + + if (!array_key_exists($other, $eventGroup)) { + return false; + } + + $events = $eventGroup[$other]; + + if (count($events) > 1) { + throw new AssertionFailedError(sprintf('Event "%s" was fired %d times, cannot make data assertion', $other, count($events))); + } + + /* @var \Cake\Event\Event $event */ + $event = $events[0]; + + if (array_key_exists($this->_dataKey, $event->getData()) === false) { + return false; + } + + return $event->getData($this->_dataKey) === $this->_dataValue; + } + + /** + * Assertion message string + * + * @return string + */ + public function toString() + { + return 'was fired with ' . $this->_dataKey . ' matching ' . (string)$this->_dataValue; + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/EmailAssertTrait.php b/app/vendor/cakephp/cakephp/src/TestSuite/EmailAssertTrait.php new file mode 100644 index 000000000..d0353a788 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/EmailAssertTrait.php @@ -0,0 +1,292 @@ +email(true)->send($content); + } + + /** + * Creates an email instance overriding its transport for testing purposes. + * + * @param bool $new Tells if new instance should forcibly be created. + * @return \Cake\Mailer\Email + */ + public function email($new = false) + { + if ($new || !$this->_email) { + $this->_email = new Email(); + $this->_email->setProfile(['transport' => 'debug'] + $this->_email->getProfile()); + } + + return $this->_email; + } + + /** + * Generates mock for given mailer class. + * + * @param string $className The mailer's FQCN. + * @param array $methods The methods to mock on the mailer. + * @return \Cake\Mailer\Mailer|\PHPUnit_Framework_MockObject_MockObject + */ + public function getMockForMailer($className, array $methods = []) + { + $name = current(array_slice(explode('\\', $className), -1)); + + if (!in_array('profile', $methods)) { + $methods[] = 'profile'; + } + + $mailer = $this->getMockBuilder($className) + ->setMockClassName($name) + ->setMethods($methods) + ->setConstructorArgs([$this->email()]) + ->getMock(); + + $mailer->expects($this->any()) + ->method('profile') + ->willReturn($mailer); + + return $mailer; + } + + /** + * Asserts email content (both text and HTML) contains `$needle`. + * + * @param string $needle Text to look for. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailMessageContains($needle, $message = null) + { + $this->assertEmailHtmlMessageContains($needle, $message); + $this->assertEmailTextMessageContains($needle, $message); + } + + /** + * Asserts HTML email content contains `$needle`. + * + * @param string $needle Text to look for. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailHtmlMessageContains($needle, $message = null) + { + $haystack = $this->email()->message('html'); + $this->assertTextContains($needle, $haystack, $message); + } + + /** + * Asserts text email content contains `$needle`. + * + * @param string $needle Text to look for. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailTextMessageContains($needle, $message = null) + { + $haystack = $this->email()->message('text'); + $this->assertTextContains($needle, $haystack, $message); + } + + /** + * Asserts email's subject contains `$expected`. + * + * @param string $expected Email's subject. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailSubject($expected, $message = null) + { + $result = $this->email()->getSubject(); + $this->assertSame($expected, $result, $message); + } + + /** + * Asserts email's sender email address and optionally name. + * + * @param string $email Sender's email address. + * @param string|null $name Sender's name. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailFrom($email, $name = null, $message = null) + { + if ($name === null) { + $name = $email; + } + + $expected = [$email => $name]; + $result = $this->email()->getFrom(); + $this->assertSame($expected, $result, $message); + } + + /** + * Asserts email is CC'd to only one email address (and optionally name). + * + * @param string $email CC'd email address. + * @param string|null $name CC'd person name. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailCc($email, $name = null, $message = null) + { + if ($name === null) { + $name = $email; + } + + $expected = [$email => $name]; + $result = $this->email()->getCc(); + $this->assertSame($expected, $result, $message); + } + + /** + * Asserts email CC'd addresses contain given email address (and + * optionally name). + * + * @param string $email CC'd email address. + * @param string|null $name CC'd person name. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailCcContains($email, $name = null, $message = null) + { + $result = $this->email()->getCc(); + $this->assertNotEmpty($result[$email], $message); + if ($name !== null) { + $this->assertEquals($result[$email], $name, $message); + } + } + + /** + * Asserts email is BCC'd to only one email address (and optionally name). + * + * @param string $email BCC'd email address. + * @param string|null $name BCC'd person name. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailBcc($email, $name = null, $message = null) + { + if ($name === null) { + $name = $email; + } + + $expected = [$email => $name]; + $result = $this->email()->getBcc(); + $this->assertSame($expected, $result, $message); + } + + /** + * Asserts email BCC'd addresses contain given email address (and + * optionally name). + * + * @param string $email BCC'd email address. + * @param string|null $name BCC'd person name. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailBccContains($email, $name = null, $message = null) + { + $result = $this->email()->getBcc(); + $this->assertNotEmpty($result[$email], $message); + if ($name !== null) { + $this->assertEquals($result[$email], $name, $message); + } + } + + /** + * Asserts email is sent to only the given recipient's address (and + * optionally name). + * + * @param string $email Recipient's email address. + * @param string|null $name Recipient's name. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailTo($email, $name = null, $message = null) + { + if ($name === null) { + $name = $email; + } + + $expected = [$email => $name]; + $result = $this->email()->getTo(); + $this->assertSame($expected, $result, $message); + } + + /** + * Asserts email recipients' list contains given email address (and + * optionally name). + * + * @param string $email Recipient's email address. + * @param string|null $name Recipient's name. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailToContains($email, $name = null, $message = null) + { + $result = $this->email()->getTo(); + $this->assertNotEmpty($result[$email], $message); + if ($name !== null) { + $this->assertEquals($result[$email], $name, $message); + } + } + + /** + * Asserts the email attachments contain the given filename (and optionally + * file info). + * + * @param string $filename Expected attachment's filename. + * @param array|null $file Expected attachment's file info. + * @param string|null $message The failure message to define. + * @return void + */ + public function assertEmailAttachmentsContains($filename, array $file = null, $message = null) + { + $result = $this->email()->getAttachments(); + $this->assertNotEmpty($result[$filename], $message); + if ($file === null) { + return; + } + $this->assertContains($file, $result, $message); + $this->assertEquals($file, $result[$filename], $message); + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php new file mode 100644 index 000000000..74edb48a3 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php @@ -0,0 +1,115 @@ +setDebug(in_array('--debug', $_SERVER['argv'])); + } + $this->_fixtureManager = $manager; + $this->_fixtureManager->shutDown(); + } + + /** + * Iterates the tests inside a test suite and creates the required fixtures as + * they were expressed inside each test case. + * + * @param \PHPUnit\Framework\TestSuite $suite The test suite + * @return void + */ + public function startTestSuite(TestSuite $suite) + { + if (empty($this->_first)) { + $this->_first = $suite; + } + } + + /** + * Destroys the fixtures created by the fixture manager at the end of the test + * suite run + * + * @param \PHPUnit\Framework\TestSuite $suite The test suite + * @return void + */ + public function endTestSuite(TestSuite $suite) + { + if ($this->_first === $suite) { + $this->_fixtureManager->shutDown(); + } + } + + /** + * Adds fixtures to a test case when it starts. + * + * @param \PHPUnit\Framework\Test $test The test case + * @return void + */ + public function startTest(Test $test) + { + $test->fixtureManager = $this->_fixtureManager; + if ($test instanceof TestCase) { + $this->_fixtureManager->fixturize($test); + $this->_fixtureManager->load($test); + } + } + + /** + * Unloads fixtures from the test case. + * + * @param \PHPUnit\Framework\Test $test The test case + * @param float $time current time + * @return void + */ + public function endTest(Test $test, $time) + { + if ($test instanceof TestCase) { + $this->_fixtureManager->unload($test); + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureManager.php b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureManager.php new file mode 100644 index 000000000..7f2d7e6fa --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureManager.php @@ -0,0 +1,500 @@ +_debug = $debug; + } + + /** + * Inspects the test to look for unloaded fixtures and loads them + * + * @param \Cake\TestSuite\TestCase $test The test case to inspect. + * @return void + */ + public function fixturize($test) + { + $this->_initDb(); + if (empty($test->fixtures) || !empty($this->_processed[get_class($test)])) { + return; + } + if (!is_array($test->fixtures)) { + $test->fixtures = array_map('trim', explode(',', $test->fixtures)); + } + $this->_loadFixtures($test); + $this->_processed[get_class($test)] = true; + } + + /** + * Get the loaded fixtures. + * + * @return array + */ + public function loaded() + { + return $this->_loaded; + } + + /** + * Add aliases for all non test prefixed connections. + * + * This allows models to use the test connections without + * a pile of configuration work. + * + * @return void + */ + protected function _aliasConnections() + { + $connections = ConnectionManager::configured(); + ConnectionManager::alias('test', 'default'); + $map = []; + foreach ($connections as $connection) { + if ($connection === 'test' || $connection === 'default') { + continue; + } + if (isset($map[$connection])) { + continue; + } + if (strpos($connection, 'test_') === 0) { + $map[$connection] = substr($connection, 5); + } else { + $map['test_' . $connection] = $connection; + } + } + foreach ($map as $testConnection => $normal) { + ConnectionManager::alias($testConnection, $normal); + } + } + + /** + * Initializes this class with a DataSource object to use as default for all fixtures + * + * @return void + */ + protected function _initDb() + { + if ($this->_initialized) { + return; + } + $this->_aliasConnections(); + $this->_initialized = true; + } + + /** + * Looks for fixture files and instantiates the classes accordingly + * + * @param \Cake\TestSuite\TestCase $test The test suite to load fixtures for. + * @return void + * @throws \UnexpectedValueException when a referenced fixture does not exist. + */ + protected function _loadFixtures($test) + { + if (empty($test->fixtures)) { + return; + } + foreach ($test->fixtures as $fixture) { + if (isset($this->_loaded[$fixture])) { + continue; + } + + if (strpos($fixture, '.')) { + list($type, $pathName) = explode('.', $fixture, 2); + $path = explode('/', $pathName); + $name = array_pop($path); + $additionalPath = implode('\\', $path); + + if ($type === 'core') { + $baseNamespace = 'Cake'; + } elseif ($type === 'app') { + $baseNamespace = Configure::read('App.namespace'); + } elseif ($type === 'plugin') { + list($plugin, $name) = explode('.', $pathName); + // Flip vendored plugin separators + $path = str_replace('/', '\\', $plugin); + $baseNamespace = Inflector::camelize(str_replace('\\', '\ ', $path)); + $additionalPath = null; + } else { + $baseNamespace = ''; + $name = $fixture; + } + + // Tweak subdirectory names, so camelize() can make the correct name + if (strpos($name, '/') > 0) { + $name = str_replace('/', '\\ ', $name); + } + + $name = Inflector::camelize($name); + $nameSegments = [ + $baseNamespace, + 'Test\Fixture', + $additionalPath, + $name . 'Fixture' + ]; + $className = implode('\\', array_filter($nameSegments)); + } else { + $className = $fixture; + $name = preg_replace('/Fixture\z/', '', substr(strrchr($fixture, '\\'), 1)); + } + + if (class_exists($className)) { + $this->_loaded[$fixture] = new $className(); + $this->_fixtureMap[$name] = $this->_loaded[$fixture]; + } else { + $msg = sprintf( + 'Referenced fixture class "%s" not found. Fixture "%s" was referenced in test case "%s".', + $className, + $fixture, + get_class($test) + ); + throw new UnexpectedValueException($msg); + } + } + } + + /** + * Runs the drop and create commands on the fixtures if necessary. + * + * @param \Cake\Datasource\FixtureInterface $fixture the fixture object to create + * @param \Cake\Database\Connection $db The Connection object instance to use + * @param array $sources The existing tables in the datasource. + * @param bool $drop whether drop the fixture if it is already created or not + * @return void + */ + protected function _setupTable($fixture, $db, array $sources, $drop = true) + { + $configName = $db->configName(); + $isFixtureSetup = $this->isFixtureSetup($configName, $fixture); + if ($isFixtureSetup) { + return; + } + + $table = $fixture->sourceName(); + $exists = in_array($table, $sources); + + $hasSchema = $fixture instanceof TableSchemaAwareInterface && $fixture->getTableSchema() instanceof TableSchema; + + if (($drop && $exists) || ($exists && !$isFixtureSetup && $hasSchema)) { + $fixture->drop($db); + $fixture->create($db); + } elseif (!$exists) { + $fixture->create($db); + } else { + $fixture->truncate($db); + } + + $this->_insertionMap[$configName][] = $fixture; + } + + /** + * Creates the fixtures tables and inserts data on them. + * + * @param \Cake\TestSuite\TestCase $test The test to inspect for fixture loading. + * @return void + * @throws \Cake\Core\Exception\Exception When fixture records cannot be inserted. + */ + public function load($test) + { + if (empty($test->fixtures)) { + return; + } + + $fixtures = $test->fixtures; + if (empty($fixtures) || !$test->autoFixtures) { + return; + } + + try { + $createTables = function ($db, $fixtures) use ($test) { + $tables = $db->getSchemaCollection()->listTables(); + $configName = $db->configName(); + if (!isset($this->_insertionMap[$configName])) { + $this->_insertionMap[$configName] = []; + } + + foreach ($fixtures as $fixture) { + if (in_array($fixture->table, $tables)) { + try { + $fixture->dropConstraints($db); + } catch (PDOException $e) { + $msg = sprintf( + 'Unable to drop constraints for fixture "%s" in "%s" test case: ' . "\n" . '%s', + get_class($fixture), + get_class($test), + $e->getMessage() + ); + throw new Exception($msg, null, $e); + } + } + } + + foreach ($fixtures as $fixture) { + if (!in_array($fixture, $this->_insertionMap[$configName])) { + $this->_setupTable($fixture, $db, $tables, $test->dropTables); + } else { + $fixture->truncate($db); + } + } + + foreach ($fixtures as $fixture) { + try { + $fixture->createConstraints($db); + } catch (PDOException $e) { + $msg = sprintf( + 'Unable to create constraints for fixture "%s" in "%s" test case: ' . "\n" . '%s', + get_class($fixture), + get_class($test), + $e->getMessage() + ); + throw new Exception($msg, null, $e); + } + } + }; + $this->_runOperation($fixtures, $createTables); + + // Use a separate transaction because of postgres. + $insert = function ($db, $fixtures) use ($test) { + foreach ($fixtures as $fixture) { + try { + $fixture->insert($db); + } catch (PDOException $e) { + $msg = sprintf( + 'Unable to insert fixture "%s" in "%s" test case: ' . "\n" . '%s', + get_class($fixture), + get_class($test), + $e->getMessage() + ); + throw new Exception($msg, null, $e); + } + } + }; + $this->_runOperation($fixtures, $insert); + } catch (PDOException $e) { + $msg = sprintf( + 'Unable to insert fixtures for "%s" test case. %s', + get_class($test), + $e->getMessage() + ); + throw new Exception($msg, null, $e); + } + } + + /** + * Run a function on each connection and collection of fixtures. + * + * @param array $fixtures A list of fixtures to operate on. + * @param callable $operation The operation to run on each connection + fixture set. + * @return void + */ + protected function _runOperation($fixtures, $operation) + { + $dbs = $this->_fixtureConnections($fixtures); + foreach ($dbs as $connection => $fixtures) { + $db = ConnectionManager::get($connection); + $logQueries = $db->logQueries(); + if ($logQueries && !$this->_debug) { + $db->logQueries(false); + } + $db->transactional(function ($db) use ($fixtures, $operation) { + $db->disableConstraints(function ($db) use ($fixtures, $operation) { + $operation($db, $fixtures); + }); + }); + if ($logQueries) { + $db->logQueries(true); + } + } + } + + /** + * Get the unique list of connections that a set of fixtures contains. + * + * @param array $fixtures The array of fixtures a list of connections is needed from. + * @return array An array of connection names. + */ + protected function _fixtureConnections($fixtures) + { + $dbs = []; + foreach ($fixtures as $f) { + if (!empty($this->_loaded[$f])) { + $fixture = $this->_loaded[$f]; + $dbs[$fixture->connection()][$f] = $fixture; + } + } + + return $dbs; + } + + /** + * Truncates the fixtures tables + * + * @param \Cake\TestSuite\TestCase $test The test to inspect for fixture unloading. + * @return void + */ + public function unload($test) + { + if (empty($test->fixtures)) { + return; + } + $truncate = function ($db, $fixtures) { + $configName = $db->configName(); + + foreach ($fixtures as $name => $fixture) { + if ($this->isFixtureSetup($configName, $fixture)) { + $fixture->dropConstraints($db); + } + } + + foreach ($fixtures as $fixture) { + if ($this->isFixtureSetup($configName, $fixture)) { + $fixture->truncate($db); + } + } + }; + $this->_runOperation($test->fixtures, $truncate); + } + + /** + * Creates a single fixture table and loads data into it. + * + * @param string $name of the fixture + * @param \Cake\Datasource\ConnectionInterface|null $db Connection instance or leave null to get a Connection from the fixture + * @param bool $dropTables Whether or not tables should be dropped and re-created. + * @return void + * @throws \UnexpectedValueException if $name is not a previously loaded class + */ + public function loadSingle($name, $db = null, $dropTables = true) + { + if (!isset($this->_fixtureMap[$name])) { + throw new UnexpectedValueException(sprintf('Referenced fixture class %s not found', $name)); + } + + $fixture = $this->_fixtureMap[$name]; + if (!$db) { + $db = ConnectionManager::get($fixture->connection()); + } + + if (!$this->isFixtureSetup($db->configName(), $fixture)) { + $sources = $db->getSchemaCollection()->listTables(); + $this->_setupTable($fixture, $db, $sources, $dropTables); + } + + if (!$dropTables) { + $fixture->dropConstraints($db); + $fixture->truncate($db); + } + + $fixture->createConstraints($db); + $fixture->insert($db); + } + + /** + * Drop all fixture tables loaded by this class + * + * @return void + */ + public function shutDown() + { + $shutdown = function ($db, $fixtures) { + $connection = $db->configName(); + foreach ($fixtures as $fixture) { + if ($this->isFixtureSetup($connection, $fixture)) { + $fixture->drop($db); + $index = array_search($fixture, $this->_insertionMap[$connection]); + unset($this->_insertionMap[$connection][$index]); + } + } + }; + $this->_runOperation(array_keys($this->_loaded), $shutdown); + } + + /** + * Check whether or not a fixture has been inserted in a given connection name. + * + * @param string $connection The connection name. + * @param \Cake\Datasource\FixtureInterface $fixture The fixture to check. + * @return bool + */ + public function isFixtureSetup($connection, $fixture) + { + return isset($this->_insertionMap[$connection]) && in_array($fixture, $this->_insertionMap[$connection]); + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/TestFixture.php b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/TestFixture.php new file mode 100644 index 000000000..5a0ec612d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/TestFixture.php @@ -0,0 +1,475 @@ +connection)) { + $connection = $this->connection; + if (strpos($connection, 'test') !== 0) { + $message = sprintf( + 'Invalid datasource name "%s" for "%s" fixture. Fixture datasource names must begin with "test".', + $connection, + $this->table + ); + throw new CakeException($message); + } + } + $this->init(); + } + + /** + * {@inheritDoc} + */ + public function connection() + { + return $this->connection; + } + + /** + * {@inheritDoc} + */ + public function sourceName() + { + return $this->table; + } + + /** + * Initialize the fixture. + * + * @return void + * @throws \Cake\ORM\Exception\MissingTableClassException When importing from a table that does not exist. + */ + public function init() + { + if ($this->table === null) { + $this->table = $this->_tableFromClass(); + } + + if (empty($this->import) && !empty($this->fields)) { + $this->_schemaFromFields(); + } + + if (!empty($this->import)) { + $this->_schemaFromImport(); + } + + if (empty($this->import) && empty($this->fields)) { + $this->_schemaFromReflection(); + } + } + + /** + * Returns the table name using the fixture class + * + * @return string + */ + protected function _tableFromClass() + { + list(, $class) = namespaceSplit(get_class($this)); + preg_match('/^(.*)Fixture$/', $class, $matches); + $table = $class; + + if (isset($matches[1])) { + $table = $matches[1]; + } + + return Inflector::tableize($table); + } + + /** + * Build the fixtures table schema from the fields property. + * + * @return void + */ + protected function _schemaFromFields() + { + $connection = ConnectionManager::get($this->connection()); + $this->_schema = new TableSchema($this->table); + foreach ($this->fields as $field => $data) { + if ($field === '_constraints' || $field === '_indexes' || $field === '_options') { + continue; + } + $this->_schema->addColumn($field, $data); + } + if (!empty($this->fields['_constraints'])) { + foreach ($this->fields['_constraints'] as $name => $data) { + if (!$connection->supportsDynamicConstraints() || $data['type'] !== TableSchema::CONSTRAINT_FOREIGN) { + $this->_schema->addConstraint($name, $data); + } else { + $this->_constraints[$name] = $data; + } + } + } + if (!empty($this->fields['_indexes'])) { + foreach ($this->fields['_indexes'] as $name => $data) { + $this->_schema->addIndex($name, $data); + } + } + if (!empty($this->fields['_options'])) { + $this->_schema->setOptions($this->fields['_options']); + } + } + + /** + * Build fixture schema from a table in another datasource. + * + * @return void + * @throws \Cake\Core\Exception\Exception when trying to import from an empty table. + */ + protected function _schemaFromImport() + { + if (!is_array($this->import)) { + return; + } + $import = $this->import + ['connection' => 'default', 'table' => null, 'model' => null]; + + if (!empty($import['model'])) { + if (!empty($import['table'])) { + throw new CakeException('You cannot define both table and model.'); + } + $import['table'] = $this->getTableLocator()->get($import['model'])->getTable(); + } + + if (empty($import['table'])) { + throw new CakeException('Cannot import from undefined table.'); + } + + $this->table = $import['table']; + + $db = ConnectionManager::get($import['connection'], false); + $schemaCollection = $db->getSchemaCollection(); + $table = $schemaCollection->describe($import['table']); + $this->_schema = $table; + } + + /** + * Build fixture schema directly from the datasource + * + * @return void + * @throws \Cake\Core\Exception\Exception when trying to reflect a table that does not exist + */ + protected function _schemaFromReflection() + { + $db = ConnectionManager::get($this->connection()); + $schemaCollection = $db->getSchemaCollection(); + $tables = $schemaCollection->listTables(); + + if (!in_array($this->table, $tables)) { + throw new CakeException( + sprintf( + 'Cannot describe schema for table `%s` for fixture `%s` : the table does not exist.', + $this->table, + get_class($this) + ) + ); + } + + $this->_schema = $schemaCollection->describe($this->table); + } + + /** + * Gets/Sets the TableSchema instance used by this fixture. + * + * @param \Cake\Database\Schema\TableSchema|null $schema The table to set. + * @return \Cake\Database\Schema\TableSchema|null + * @deprecated 3.5.0 Use getTableSchema/setTableSchema instead. + */ + public function schema(TableSchema $schema = null) + { + deprecationWarning( + 'TestFixture::schema() is deprecated. ' . + 'Use TestFixture::setTableSchema()/getTableSchema() instead.' + ); + if ($schema) { + $this->setTableSchema($schema); + } + + return $this->getTableSchema(); + } + + /** + * {@inheritDoc} + */ + public function create(ConnectionInterface $db) + { + if (empty($this->_schema)) { + return false; + } + + if (empty($this->import) && empty($this->fields)) { + return true; + } + + try { + $queries = $this->_schema->createSql($db); + foreach ($queries as $query) { + $stmt = $db->prepare($query); + $stmt->execute(); + $stmt->closeCursor(); + } + } catch (Exception $e) { + $msg = sprintf( + 'Fixture creation for "%s" failed "%s"', + $this->table, + $e->getMessage() + ); + Log::error($msg); + trigger_error($msg, E_USER_WARNING); + + return false; + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function drop(ConnectionInterface $db) + { + if (empty($this->_schema)) { + return false; + } + + if (empty($this->import) && empty($this->fields)) { + return true; + } + + try { + $sql = $this->_schema->dropSql($db); + foreach ($sql as $stmt) { + $db->execute($stmt)->closeCursor(); + } + } catch (Exception $e) { + return false; + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function insert(ConnectionInterface $db) + { + if (isset($this->records) && !empty($this->records)) { + list($fields, $values, $types) = $this->_getRecords(); + $query = $db->newQuery() + ->insert($fields, $types) + ->into($this->table); + + foreach ($values as $row) { + $query->values($row); + } + $statement = $query->execute(); + $statement->closeCursor(); + + return $statement; + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function createConstraints(ConnectionInterface $db) + { + if (empty($this->_constraints)) { + return true; + } + + foreach ($this->_constraints as $name => $data) { + $this->_schema->addConstraint($name, $data); + } + + $sql = $this->_schema->addConstraintSql($db); + + if (empty($sql)) { + return true; + } + + foreach ($sql as $stmt) { + $db->execute($stmt)->closeCursor(); + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function dropConstraints(ConnectionInterface $db) + { + if (empty($this->_constraints)) { + return true; + } + + $sql = $this->_schema->dropConstraintSql($db); + + if (empty($sql)) { + return true; + } + + foreach ($sql as $stmt) { + $db->execute($stmt)->closeCursor(); + } + + foreach ($this->_constraints as $name => $data) { + $this->_schema->dropConstraint($name); + } + + return true; + } + + /** + * Converts the internal records into data used to generate a query. + * + * @return array + */ + protected function _getRecords() + { + $fields = $values = $types = []; + $columns = $this->_schema->columns(); + foreach ($this->records as $record) { + $fields = array_merge($fields, array_intersect(array_keys($record), $columns)); + } + $fields = array_values(array_unique($fields)); + foreach ($fields as $field) { + $types[$field] = $this->_schema->getColumn($field)['type']; + } + $default = array_fill_keys($fields, null); + foreach ($this->records as $record) { + $values[] = array_merge($default, $record); + } + + return [$fields, $values, $types]; + } + + /** + * {@inheritDoc} + */ + public function truncate(ConnectionInterface $db) + { + $sql = $this->_schema->truncateSql($db); + foreach ($sql as $stmt) { + $db->execute($stmt)->closeCursor(); + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function getTableSchema() + { + return $this->_schema; + } + + /** + * {@inheritDoc} + */ + public function setTableSchema(DatabaseTableSchemaInterface $schema) + { + $this->_schema = $schema; + + return $this; + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/IntegrationTestCase.php b/app/vendor/cakephp/cakephp/src/TestSuite/IntegrationTestCase.php new file mode 100644 index 000000000..f19f7a670 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/IntegrationTestCase.php @@ -0,0 +1,1220 @@ +_useHttpServer = class_exists($namespace . '\Application'); + } + + /** + * Clears the state used for requests. + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + $this->_request = []; + $this->_session = []; + $this->_cookie = []; + $this->_response = null; + $this->_exception = null; + $this->_controller = null; + $this->_viewName = null; + $this->_layoutName = null; + $this->_requestSession = null; + $this->_appClass = null; + $this->_appArgs = null; + $this->_securityToken = false; + $this->_csrfToken = false; + $this->_retainFlashMessages = false; + $this->_useHttpServer = false; + } + + /** + * Toggle whether or not you want to use the HTTP Server stack. + * + * @param bool $enable Enable/disable the usage of the HTTP Stack. + * @return void + */ + public function useHttpServer($enable) + { + $this->_useHttpServer = (bool)$enable; + } + + /** + * Configure the application class to use in integration tests. + * + * Combined with `useHttpServer()` to customize the class name and constructor arguments + * of your application class. + * + * @param string $class The application class name. + * @param array|null $constructorArgs The constructor arguments for your application class. + * @return void + */ + public function configApplication($class, $constructorArgs) + { + $this->_appClass = $class; + $this->_appArgs = $constructorArgs; + } + + /** + * Calling this method will enable a SecurityComponent + * compatible token to be added to request data. This + * lets you easily test actions protected by SecurityComponent. + * + * @return void + */ + public function enableSecurityToken() + { + $this->_securityToken = true; + } + + /** + * Calling this method will add a CSRF token to the request. + * + * Both the POST data and cookie will be populated when this option + * is enabled. The default parameter names will be used. + * + * @return void + */ + public function enableCsrfToken() + { + $this->_csrfToken = true; + } + + /** + * Calling this method will re-store flash messages into the test session + * after being removed by the FlashHelper + * + * @return void + */ + public function enableRetainFlashMessages() + { + $this->_retainFlashMessages = true; + } + + /** + * Configures the data for the *next* request. + * + * This data is cleared in the tearDown() method. + * + * You can call this method multiple times to append into + * the current state. + * + * @param array $data The request data to use. + * @return void + */ + public function configRequest(array $data) + { + $this->_request = $data + $this->_request; + } + + /** + * Sets session data. + * + * This method lets you configure the session data + * you want to be used for requests that follow. The session + * state is reset in each tearDown(). + * + * You can call this method multiple times to append into + * the current state. + * + * @param array $data The session data to use. + * @return void + */ + public function session(array $data) + { + $this->_session = $data + $this->_session; + } + + /** + * Sets a request cookie for future requests. + * + * This method lets you configure the session data + * you want to be used for requests that follow. The session + * state is reset in each tearDown(). + * + * You can call this method multiple times to append into + * the current state. + * + * @param string $name The cookie name to use. + * @param mixed $value The value of the cookie. + * @return void + */ + public function cookie($name, $value) + { + $this->_cookie[$name] = $value; + } + + /** + * Returns the encryption key to be used. + * + * @return string + */ + protected function _getCookieEncryptionKey() + { + if (isset($this->_cookieEncryptionKey)) { + return $this->_cookieEncryptionKey; + } + + return Security::getSalt(); + } + + /** + * Sets a encrypted request cookie for future requests. + * + * The difference from cookie() is this encrypts the cookie + * value like the CookieComponent. + * + * @param string $name The cookie name to use. + * @param mixed $value The value of the cookie. + * @param string|bool $encrypt Encryption mode to use. + * @param string|null $key Encryption key used. Defaults + * to Security.salt. + * @return void + * @see \Cake\Utility\CookieCryptTrait::_encrypt() + */ + public function cookieEncrypted($name, $value, $encrypt = 'aes', $key = null) + { + $this->_cookieEncryptionKey = $key; + $this->_cookie[$name] = $this->_encrypt($value, $encrypt); + } + + /** + * Performs a GET request using the current request data. + * + * The response of the dispatched request will be stored as + * a property. You can use various assert methods to check the + * response. + * + * @param string|array $url The URL to request. + * @return void + * @throws \PHPUnit\Exception + */ + public function get($url) + { + $this->_sendRequest($url, 'GET'); + } + + /** + * Performs a POST request using the current request data. + * + * The response of the dispatched request will be stored as + * a property. You can use various assert methods to check the + * response. + * + * @param string|array $url The URL to request. + * @param array $data The data for the request. + * @return void + * @throws \PHPUnit\Exception + */ + public function post($url, $data = []) + { + $this->_sendRequest($url, 'POST', $data); + } + + /** + * Performs a PATCH request using the current request data. + * + * The response of the dispatched request will be stored as + * a property. You can use various assert methods to check the + * response. + * + * @param string|array $url The URL to request. + * @param array $data The data for the request. + * @return void + * @throws \PHPUnit\Exception + */ + public function patch($url, $data = []) + { + $this->_sendRequest($url, 'PATCH', $data); + } + + /** + * Performs a PUT request using the current request data. + * + * The response of the dispatched request will be stored as + * a property. You can use various assert methods to check the + * response. + * + * @param string|array $url The URL to request. + * @param array $data The data for the request. + * @return void + * @throws \PHPUnit\Exception + */ + public function put($url, $data = []) + { + $this->_sendRequest($url, 'PUT', $data); + } + + /** + * Performs a DELETE request using the current request data. + * + * The response of the dispatched request will be stored as + * a property. You can use various assert methods to check the + * response. + * + * @param string|array $url The URL to request. + * @return void + * @throws \PHPUnit\Exception + */ + public function delete($url) + { + $this->_sendRequest($url, 'DELETE'); + } + + /** + * Performs a HEAD request using the current request data. + * + * The response of the dispatched request will be stored as + * a property. You can use various assert methods to check the + * response. + * + * @param string|array $url The URL to request. + * @return void + * @throws \PHPUnit\Exception + */ + public function head($url) + { + $this->_sendRequest($url, 'HEAD'); + } + + /** + * Performs an OPTIONS request using the current request data. + * + * The response of the dispatched request will be stored as + * a property. You can use various assert methods to check the + * response. + * + * @param string|array $url The URL to request. + * @return void + * @throws \PHPUnit\Exception + */ + public function options($url) + { + $this->_sendRequest($url, 'OPTIONS'); + } + + /** + * Creates and send the request into a Dispatcher instance. + * + * Receives and stores the response for future inspection. + * + * @param string|array $url The URL + * @param string $method The HTTP method + * @param array|null $data The request data. + * @return void + * @throws \PHPUnit\Exception + */ + protected function _sendRequest($url, $method, $data = []) + { + $dispatcher = $this->_makeDispatcher(); + $url = $dispatcher->resolveUrl($url); + + try { + $request = $this->_buildRequest($url, $method, $data); + $response = $dispatcher->execute($request); + $this->_requestSession = $request['session']; + if ($this->_retainFlashMessages && $this->_flashMessages) { + $this->_requestSession->write('Flash', $this->_flashMessages); + } + $this->_response = $response; + } catch (PhpUnitException $e) { + throw $e; + } catch (DatabaseException $e) { + throw $e; + } catch (LogicException $e) { + throw $e; + } catch (Exception $e) { + $this->_exception = $e; + $this->_handleError($e); + } + } + + /** + * Get the correct dispatcher instance. + * + * @return \Cake\TestSuite\MiddlewareDispatcher|\Cake\TestSuite\LegacyRequestDispatcher A dispatcher instance + */ + protected function _makeDispatcher() + { + if ($this->_useHttpServer) { + return new MiddlewareDispatcher($this, $this->_appClass, $this->_appArgs); + } + + return new LegacyRequestDispatcher($this); + } + + /** + * Adds additional event spies to the controller/view event manager. + * + * @param \Cake\Event\Event $event A dispatcher event. + * @param \Cake\Controller\Controller|null $controller Controller instance. + * @return void + */ + public function controllerSpy($event, $controller = null) + { + if (!$controller) { + /** @var \Cake\Controller\Controller $controller */ + $controller = $event->getSubject(); + } + $this->_controller = $controller; + $events = $controller->getEventManager(); + $events->on('View.beforeRender', function ($event, $viewFile) use ($controller) { + if (!$this->_viewName) { + $this->_viewName = $viewFile; + } + if ($this->_retainFlashMessages) { + $this->_flashMessages = $controller->getRequest()->getSession()->read('Flash'); + } + }); + $events->on('View.beforeLayout', function ($event, $viewFile) { + $this->_layoutName = $viewFile; + }); + } + + /** + * Attempts to render an error response for a given exception. + * + * This method will attempt to use the configured exception renderer. + * If that class does not exist, the built-in renderer will be used. + * + * @param \Exception $exception Exception to handle. + * @return void + * @throws \Exception + */ + protected function _handleError($exception) + { + $class = Configure::read('Error.exceptionRenderer'); + if (empty($class) || !class_exists($class)) { + $class = 'Cake\Error\ExceptionRenderer'; + } + /** @var \Cake\Error\ExceptionRenderer $instance */ + $instance = new $class($exception); + $this->_response = $instance->render(); + } + + /** + * Creates a request object with the configured options and parameters. + * + * @param string|array $url The URL + * @param string $method The HTTP method + * @param array|null $data The request data. + * @return array The request context + */ + protected function _buildRequest($url, $method, $data) + { + $sessionConfig = (array)Configure::read('Session') + [ + 'defaults' => 'php', + ]; + $session = Session::create($sessionConfig); + $session->write($this->_session); + list ($url, $query) = $this->_url($url); + $tokenUrl = $url; + + if ($query) { + $tokenUrl .= '?' . $query; + } + + parse_str($query, $queryData); + $props = [ + 'url' => $url, + 'session' => $session, + 'query' => $queryData + ]; + if (is_string($data)) { + $props['input'] = $data; + } + if (!isset($props['input'])) { + $data = $this->_addTokens($tokenUrl, $data); + $props['post'] = $this->_castToString($data); + } + $props['cookies'] = $this->_cookie; + + $env = [ + 'REQUEST_METHOD' => $method, + 'QUERY_STRING' => $query, + 'REQUEST_URI' => $url, + ]; + if (isset($this->_request['headers'])) { + foreach ($this->_request['headers'] as $k => $v) { + $name = strtoupper(str_replace('-', '_', $k)); + if (!in_array($name, ['CONTENT_LENGTH', 'CONTENT_TYPE'])) { + $name = 'HTTP_' . $name; + } + $env[$name] = $v; + } + unset($this->_request['headers']); + } + $props['environment'] = $env; + $props = Hash::merge($props, $this->_request); + + return $props; + } + + /** + * Add the CSRF and Security Component tokens if necessary. + * + * @param string $url The URL the form is being submitted on. + * @param array $data The request body data. + * @return array The request body with tokens added. + */ + protected function _addTokens($url, $data) + { + if ($this->_securityToken === true) { + $keys = array_map(function ($field) { + return preg_replace('/(\.\d+)+$/', '', $field); + }, array_keys(Hash::flatten($data))); + $tokenData = $this->_buildFieldToken($url, array_unique($keys)); + $data['_Token'] = $tokenData; + $data['_Token']['debug'] = 'SecurityComponent debug data would be added here'; + } + + if ($this->_csrfToken === true) { + if (!isset($this->_cookie['csrfToken'])) { + $this->_cookie['csrfToken'] = Text::uuid(); + } + if (!isset($data['_csrfToken'])) { + $data['_csrfToken'] = $this->_cookie['csrfToken']; + } + } + + return $data; + } + + /** + * Recursively casts all data to string as that is how data would be POSTed in + * the real world + * + * @param array $data POST data + * @return array + */ + protected function _castToString($data) + { + foreach ($data as $key => $value) { + if (is_scalar($value)) { + $data[$key] = $value === false ? '0' : (string)$value; + + continue; + } + + if (is_array($value)) { + $looksLikeFile = isset($value['error'], $value['tmp_name'], $value['size']); + if ($looksLikeFile) { + continue; + } + + $data[$key] = $this->_castToString($value); + } + } + + return $data; + } + + /** + * Creates a valid request url and parameter array more like Request::_url() + * + * @param string $url The URL + * @return array Qualified URL and the query parameters + */ + protected function _url($url) + { + // re-create URL in ServerRequest's context so + // query strings are encoded as expected + $request = new ServerRequest(['url' => $url]); + $url = $request->getRequestTarget(); + + $query = ''; + + $path = parse_url($url, PHP_URL_PATH); + if (strpos($url, '?') !== false) { + $query = parse_url($url, PHP_URL_QUERY); + } + + return [$path, $query]; + } + + /** + * Get the response body as string + * + * @return string The response body. + */ + protected function _getBodyAsString() + { + return (string)$this->_response->getBody(); + } + + /** + * Fetches a view variable by name. + * + * If the view variable does not exist, null will be returned. + * + * @param string $name The view variable to get. + * @return mixed The view variable if set. + */ + public function viewVariable($name) + { + if (empty($this->_controller->viewVars)) { + $this->fail('There are no view variables, perhaps you need to run a request?'); + } + if (isset($this->_controller->viewVars[$name])) { + return $this->_controller->viewVars[$name]; + } + + return null; + } + + /** + * Asserts that the response status code is in the 2xx range. + * + * @param string $message Custom message for failure. + * @return void + */ + public function assertResponseOk($message = null) + { + if (empty($message)) { + $message = 'Status code is not between 200 and 204'; + } + $this->_assertStatus(200, 204, $message); + } + + /** + * Asserts that the response status code is in the 2xx/3xx range. + * + * @param string $message Custom message for failure. + * @return void + */ + public function assertResponseSuccess($message = null) + { + if (empty($message)) { + $message = 'Status code is not between 200 and 308'; + } + $this->_assertStatus(200, 308, $message); + } + + /** + * Asserts that the response status code is in the 4xx range. + * + * @param string $message Custom message for failure. + * @return void + */ + public function assertResponseError($message = null) + { + if (empty($message)) { + $message = 'Status code is not between 400 and 429'; + } + $this->_assertStatus(400, 429, $message); + } + + /** + * Asserts that the response status code is in the 5xx range. + * + * @param string $message Custom message for failure. + * @return void + */ + public function assertResponseFailure($message = null) + { + if (empty($message)) { + $message = 'Status code is not between 500 and 505'; + } + $this->_assertStatus(500, 505, $message); + } + + /** + * Asserts a specific response status code. + * + * @param int $code Status code to assert. + * @param string $message Custom message for failure. + * @return void + */ + public function assertResponseCode($code, $message = null) + { + $actual = $this->_response->getStatusCode(); + + if (empty($message)) { + $message = 'Status code is not ' . $code . ' but ' . $actual; + } + + $this->_assertStatus($code, $code, $message); + } + + /** + * Helper method for status assertions. + * + * @param int $min Min status code. + * @param int $max Max status code. + * @param string $message The error message. + * @return void + */ + protected function _assertStatus($min, $max, $message) + { + if (!$this->_response) { + $this->fail('No response set, cannot assert status code.'); + } + $status = $this->_response->getStatusCode(); + + if ($this->_exception && ($status < $min || $status > $max)) { + $this->fail($this->_exception->getMessage()); + } + + $this->assertGreaterThanOrEqual($min, $status, $message); + $this->assertLessThanOrEqual($max, $status, $message); + } + + /** + * Asserts that the Location header is correct. + * + * @param string|array|null $url The URL you expected the client to go to. This + * can either be a string URL or an array compatible with Router::url(). Use null to + * simply check for the existence of this header. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertRedirect($url = null, $message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert location header. ' . $message); + } + $result = $this->_response->getHeaderLine('Location'); + if ($url === null) { + $this->assertNotEmpty($result, $message); + + return; + } + if (empty($result)) { + $this->fail('No location header set. ' . $message); + } + $this->assertEquals(Router::url($url, ['_full' => true]), $result, $message); + } + + /** + * Asserts that the Location header contains a substring + * + * @param string $url The URL you expected the client to go to. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertRedirectContains($url, $message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert location header. ' . $message); + } + $result = $this->_response->getHeaderLine('Location'); + if (empty($result)) { + $this->fail('No location header set. ' . $message); + } + $this->assertContains($url, $result, $message); + } + + /** + * Asserts that the Location header is not set. + * + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertNoRedirect($message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert location header. ' . $message); + } + $result = $this->_response->getHeaderLine('Location'); + if (!$message) { + $message = 'Redirect header set'; + } + if (!empty($result)) { + $message .= ': ' . $result; + } + $this->assertEmpty($result, $message); + } + + /** + * Asserts response headers + * + * @param string $header The header to check + * @param string $content The content to check for. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertHeader($header, $content, $message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert headers. ' . $message); + } + if (!$this->_response->hasHeader($header)) { + $this->fail("The '$header' header is not set. " . $message); + } + $actual = $this->_response->getHeaderLine($header); + $this->assertEquals($content, $actual, $message); + } + + /** + * Asserts response header contains a string + * + * @param string $header The header to check + * @param string $content The content to check for. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertHeaderContains($header, $content, $message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert headers. ' . $message); + } + if (!$this->_response->hasHeader($header)) { + $this->fail("The '$header' header is not set. " . $message); + } + $actual = $this->_response->getHeaderLine($header); + $this->assertContains($content, $actual, $message); + } + + /** + * Asserts content type + * + * @param string $type The content-type to check for. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertContentType($type, $message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert content-type. ' . $message); + } + $alias = $this->_response->getMimeType($type); + if ($alias !== false) { + $type = $alias; + } + $result = $this->_response->getType(); + $this->assertEquals($type, $result, $message); + } + + /** + * Asserts content exists in the response body. + * + * @param mixed $content The content to check for. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertResponseEquals($content, $message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert content. ' . $message); + } + $this->assertEquals($content, $this->_getBodyAsString(), $message); + } + + /** + * Asserts content exists in the response body. + * + * @param string $content The content to check for. + * @param string $message The failure message that will be appended to the generated message. + * @param bool $ignoreCase A flag to check whether we should ignore case or not. + * @return void + */ + public function assertResponseContains($content, $message = '', $ignoreCase = false) + { + if (!$this->_response) { + $this->fail('No response set, cannot assert content. ' . $message); + } + $this->assertContains($content, $this->_getBodyAsString(), $message, $ignoreCase); + } + + /** + * Asserts content does not exist in the response body. + * + * @param string $content The content to check for. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertResponseNotContains($content, $message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert content. ' . $message); + } + $this->assertNotContains($content, $this->_getBodyAsString(), $message); + } + + /** + * Asserts that the response body matches a given regular expression. + * + * @param string $pattern The pattern to compare against. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertResponseRegExp($pattern, $message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert content. ' . $message); + } + $this->assertRegExp($pattern, $this->_getBodyAsString(), $message); + } + + /** + * Asserts that the response body does not match a given regular expression. + * + * @param string $pattern The pattern to compare against. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertResponseNotRegExp($pattern, $message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert content. ' . $message); + } + $this->assertNotRegExp($pattern, $this->_getBodyAsString(), $message); + } + + /** + * Assert response content is not empty. + * + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertResponseNotEmpty($message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert content. ' . $message); + } + $this->assertNotEmpty($this->_getBodyAsString(), $message); + } + /** + * Assert response content is empty. + * + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertResponseEmpty($message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert content. ' . $message); + } + $this->assertEmpty($this->_getBodyAsString(), $message); + } + + /** + * Asserts that the search string was in the template name. + * + * @param string $content The content to check for. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertTemplate($content, $message = '') + { + if (!$this->_viewName) { + $this->fail('No view name stored. ' . $message); + } + $this->assertContains($content, $this->_viewName, $message); + } + + /** + * Asserts that the search string was in the layout name. + * + * @param string $content The content to check for. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertLayout($content, $message = '') + { + if (!$this->_layoutName) { + $this->fail('No layout name stored. ' . $message); + } + $this->assertContains($content, $this->_layoutName, $message); + } + + /** + * Asserts session contents + * + * @param string $expected The expected contents. + * @param string $path The session data path. Uses Hash::get() compatible notation + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertSession($expected, $path, $message = '') + { + if (empty($this->_requestSession)) { + $this->fail('There is no stored session data. Perhaps you need to run a request?'); + } + $result = $this->_requestSession->read($path); + $this->assertEquals( + $expected, + $result, + 'Session content for "' . $path . '" differs. ' . $message + ); + } + + /** + * Asserts cookie values + * + * @param string $expected The expected contents. + * @param string $name The cookie name. + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertCookie($expected, $name, $message = '') + { + if (!$this->_response) { + $this->fail('Not response set, cannot assert cookies.'); + } + $result = $this->_response->getCookie($name); + $this->assertEquals( + $expected, + $result['value'], + 'Cookie "' . $name . '" data differs. ' . $message + ); + } + + /** + * Asserts a cookie has not been set in the response + * + * @param string $cookie The cookie name to check + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertCookieNotSet($cookie, $message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert cookies. ' . $message); + } + + $this->assertCookie(null, $cookie, "Cookie '{$cookie}' has been set. " . $message); + } + + /** + * Disable the error handler middleware. + * + * By using this function, exceptions are no longer caught by the ErrorHandlerMiddleware + * and are instead re-thrown by the TestExceptionRenderer. This can be helpful + * when trying to diagnose/debug unexpected failures in test cases. + * + * @return void + */ + public function disableErrorHandlerMiddleware() + { + Configure::write('Error.exceptionRenderer', TestExceptionRenderer::class); + } + + /** + * Asserts cookie values which are encrypted by the + * CookieComponent. + * + * The difference from assertCookie() is this decrypts the cookie + * value like the CookieComponent for this assertion. + * + * @param string $expected The expected contents. + * @param string $name The cookie name. + * @param string|bool $encrypt Encryption mode to use. + * @param string|null $key Encryption key used. Defaults + * to Security.salt. + * @param string $message The failure message that will be appended to the generated message. + * @return void + * @see \Cake\Utility\CookieCryptTrait::_encrypt() + */ + public function assertCookieEncrypted($expected, $name, $encrypt = 'aes', $key = null, $message = '') + { + if (!$this->_response) { + $this->fail('No response set, cannot assert cookies.'); + } + $result = $this->_response->getCookie($name); + $this->_cookieEncryptionKey = $key; + $result['value'] = $this->_decrypt($result['value'], $encrypt); + $this->assertEquals($expected, $result['value'], 'Cookie data differs. ' . $message); + } + + /** + * Asserts that a file with the given name was sent in the response + * + * @param string $expected The file name that should be sent in the response + * @param string $message The failure message that will be appended to the generated message. + * @return void + */ + public function assertFileResponse($expected, $message = '') + { + if ($this->_response === null) { + $this->fail('No response set, cannot assert file.'); + } + $actual = isset($this->_response->getFile()->path) ? $this->_response->getFile()->path : null; + + if ($actual === null) { + $this->fail('No file was sent in this response'); + } + $this->assertEquals($expected, $actual, $message); + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/LegacyCommandRunner.php b/app/vendor/cakephp/cakephp/src/TestSuite/LegacyCommandRunner.php new file mode 100644 index 000000000..9674c9f74 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/LegacyCommandRunner.php @@ -0,0 +1,42 @@ +dispatch(); + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/LegacyRequestDispatcher.php b/app/vendor/cakephp/cakephp/src/TestSuite/LegacyRequestDispatcher.php new file mode 100644 index 000000000..86626e145 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/LegacyRequestDispatcher.php @@ -0,0 +1,75 @@ +_test = $test; + } + + /** + * Resolve the user provided URL into the actual request URL. + * + * @param array|string $url The URL array/string to resolve. + * @return string + */ + public function resolveUrl($url) + { + return Router::url($url); + } + + /** + * Run a request and get the response. + * + * @param array $request The request context to execute. + * @return string|null The generated response. + */ + public function execute($request) + { + $request = new ServerRequest($request); + $response = new Response(); + $dispatcher = DispatcherFactory::create(); + $dispatcher->getEventManager()->on( + 'Dispatcher.invokeController', + ['priority' => 999], + [$this->_test, 'controllerSpy'] + ); + + return $dispatcher->dispatch($request, $response); + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/LegacyShellDispatcher.php b/app/vendor/cakephp/cakephp/src/TestSuite/LegacyShellDispatcher.php new file mode 100644 index 000000000..404b585b0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/LegacyShellDispatcher.php @@ -0,0 +1,56 @@ +_io = $io; + parent::__construct($args, $bootstrap); + } + + /** + * Injects mock and stub io components into the shell + * + * @param string $className Class name + * @param string $shortName Short name + * @return \Cake\Console\Shell + */ + protected function _createShell($className, $shortName) + { + list($plugin) = pluginSplit($shortName); + $instance = new $className($this->_io); + $instance->plugin = trim($plugin, '.'); + + return $instance; + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/MiddlewareDispatcher.php b/app/vendor/cakephp/cakephp/src/TestSuite/MiddlewareDispatcher.php new file mode 100644 index 000000000..1b7dcc884 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/MiddlewareDispatcher.php @@ -0,0 +1,184 @@ +_test = $test; + $this->_class = $class ?: Configure::read('App.namespace') . '\Application'; + $this->_constructorArgs = $constructorArgs ?: [CONFIG]; + + try { + $reflect = new ReflectionClass($this->_class); + $this->app = $reflect->newInstanceArgs($this->_constructorArgs); + } catch (ReflectionException $e) { + throw new LogicException(sprintf('Cannot load "%s" for use in integration testing.', $this->_class)); + } + } + + /** + * Resolve the provided URL into a string. + * + * @param array|string $url The URL array/string to resolve. + * @return string + */ + public function resolveUrl($url) + { + // If we need to resolve a Route URL but there are no routes, load routes. + if (is_array($url) && count(Router::getRouteCollection()->routes()) === 0) { + return $this->resolveRoute($url); + } + + return Router::url($url); + } + + /** + * Convert a URL array into a string URL via routing. + * + * @param array $url The url to resolve + * @return string + */ + protected function resolveRoute(array $url) + { + // Simulate application bootstrap and route loading. + // We need both to ensure plugins are loaded. + $this->app->bootstrap(); + if ($this->app instanceof PluginApplicationInterface) { + $this->app->pluginBootstrap(); + } + $builder = Router::createRouteBuilder('/'); + + if ($this->app instanceof HttpApplicationInterface) { + $this->app->routes($builder); + } + if ($this->app instanceof PluginApplicationInterface) { + $this->app->pluginRoutes($builder); + } + + $out = Router::url($url); + Router::reload(); + + return $out; + } + + /** + * Run a request and get the response. + * + * @param \Cake\Http\ServerRequest $request The request to execute. + * @return \Psr\Http\Message\ResponseInterface The generated response. + */ + public function execute($request) + { + // Spy on the controller using the initialize hook instead + // of the dispatcher hooks as those will be going away one day. + EventManager::instance()->on( + 'Controller.initialize', + [$this->_test, 'controllerSpy'] + ); + + $server = new Server($this->app); + $psrRequest = $this->_createRequest($request); + + return $server->run($psrRequest); + } + + /** + * Create a PSR7 request from the request spec. + * + * @param array $spec The request spec. + * @return \Psr\Http\Message\RequestInterface + */ + protected function _createRequest($spec) + { + if (isset($spec['input'])) { + $spec['post'] = []; + } + $environment = array_merge( + array_merge($_SERVER, ['REQUEST_URI' => $spec['url'], 'PHP_SELF' => '/']), + $spec['environment'] + ); + $request = ServerRequestFactory::fromGlobals( + $environment, + $spec['query'], + $spec['post'], + $spec['cookies'] + ); + $request = $request->withAttribute('session', $spec['session']); + + if (isset($spec['input'])) { + $stream = new Stream('php://memory', 'rw'); + $stream->write($spec['input']); + $stream->rewind(); + $request = $request->withBody($stream); + } + + return $request; + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/StringCompareTrait.php b/app/vendor/cakephp/cakephp/src/TestSuite/StringCompareTrait.php new file mode 100644 index 000000000..b6a939090 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/StringCompareTrait.php @@ -0,0 +1,71 @@ +_compareBasePath . $path; + } + + if ($this->_updateComparisons === null) { + $this->_updateComparisons = env('UPDATE_TEST_COMPARISON_FILES'); + } + + if ($this->_updateComparisons) { + $file = new File($path, true); + $file->write($result); + } + + $expected = file_get_contents($path); + $this->assertTextEquals($expected, $result); + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Stub/ConsoleOutput.php b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/ConsoleOutput.php new file mode 100644 index 000000000..f9c2b76b0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/ConsoleOutput.php @@ -0,0 +1,71 @@ +_out[] = $line; + } + + $newlines--; + while ($newlines > 0) { + $this->_out[] = ''; + $newlines--; + } + } + + /** + * Get the buffered output. + * + * @return array + */ + public function messages() + { + return $this->_out; + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Stub/Response.php b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/Response.php new file mode 100644 index 000000000..1d8b51252 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/Response.php @@ -0,0 +1,38 @@ +hasHeader('Location') && $this->_status === 200) { + $this->statusCode(302); + } + $this->_setContentType(); + + return $this; + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Stub/TestExceptionRenderer.php b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/TestExceptionRenderer.php new file mode 100644 index 000000000..5d194cf1d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/TestExceptionRenderer.php @@ -0,0 +1,43 @@ +markTestSkipped($message); + } + + return $shouldSkip; + } + + /** + * Helper method for tests that needs to use error_reporting() + * + * @param int $errorLevel value of error_reporting() that needs to use + * @param callable $callable callable function that will receive asserts + * @return void + */ + public function withErrorReporting($errorLevel, $callable) + { + $default = error_reporting(); + error_reporting($errorLevel); + try { + $callable(); + } finally { + error_reporting($default); + } + } + + /** + * Helper method for check deprecation methods + * + * @param callable $callable callable function that will receive asserts + * @return void + */ + public function deprecated($callable) + { + $errorLevel = error_reporting(); + error_reporting(E_ALL ^ E_USER_DEPRECATED); + try { + $callable(); + } finally { + error_reporting($errorLevel); + } + } + + /** + * Setup the test case, backup the static object values so they can be restored. + * Specifically backs up the contents of Configure and paths in App if they have + * not already been backed up. + * + * @return void + */ + public function setUp() + { + parent::setUp(); + + if (!$this->_configure) { + $this->_configure = Configure::read(); + } + if (class_exists('Cake\Routing\Router', false)) { + Router::reload(); + } + + EventManager::instance(new EventManager()); + } + + /** + * teardown any static object changes and restore them. + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + if ($this->_configure) { + Configure::clear(); + Configure::write($this->_configure); + } + $this->getTableLocator()->clear(); + } + + /** + * Chooses which fixtures to load for a given test + * + * Each parameter is a model name that corresponds to a fixture, i.e. 'Posts', 'Authors', etc. + * Passing no parameters will cause all fixtures on the test case to load. + * + * @return void + * @see \Cake\TestSuite\TestCase::$autoFixtures + * @throws \Exception when no fixture manager is available. + */ + public function loadFixtures() + { + if ($this->fixtureManager === null) { + throw new Exception('No fixture manager to load the test fixture'); + } + $args = func_get_args(); + foreach ($args as $class) { + $this->fixtureManager->loadSingle($class, null, $this->dropTables); + } + + if (empty($args)) { + $autoFixtures = $this->autoFixtures; + $this->autoFixtures = true; + $this->fixtureManager->load($this); + $this->autoFixtures = $autoFixtures; + } + } + + /** + * Asserts that a global event was fired. You must track events in your event manager for this assertion to work + * + * @param string $name Event name + * @param EventManager|null $eventManager Event manager to check, defaults to global event manager + * @param string $message Assertion failure message + * @return void + */ + public function assertEventFired($name, $eventManager = null, $message = '') + { + if (!$eventManager) { + $eventManager = EventManager::instance(); + } + $this->assertThat($name, new EventFired($eventManager), $message); + } + + /** + * Asserts an event was fired with data + * + * If a third argument is passed, that value is used to compare with the value in $dataKey + * + * @param string $name Event name + * @param string $dataKey Data key + * @param string $dataValue Data value + * @param EventManager|null $eventManager Event manager to check, defaults to global event manager + * @param string $message Assertion failure message + * @return void + */ + public function assertEventFiredWith($name, $dataKey, $dataValue, $eventManager = null, $message = '') + { + if (!$eventManager) { + $eventManager = EventManager::instance(); + } + $this->assertThat($name, new EventFiredWith($eventManager, $dataKey, $dataValue), $message); + } + + /** + * Assert text equality, ignoring differences in newlines. + * Helpful for doing cross platform tests of blocks of text. + * + * @param string $expected The expected value. + * @param string $result The actual value. + * @param string $message The message to use for failure. + * @return void + */ + public function assertTextNotEquals($expected, $result, $message = '') + { + $expected = str_replace(["\r\n", "\r"], "\n", $expected); + $result = str_replace(["\r\n", "\r"], "\n", $result); + $this->assertNotEquals($expected, $result, $message); + } + + /** + * Assert text equality, ignoring differences in newlines. + * Helpful for doing cross platform tests of blocks of text. + * + * @param string $expected The expected value. + * @param string $result The actual value. + * @param string $message The message to use for failure. + * @return void + */ + public function assertTextEquals($expected, $result, $message = '') + { + $expected = str_replace(["\r\n", "\r"], "\n", $expected); + $result = str_replace(["\r\n", "\r"], "\n", $result); + $this->assertEquals($expected, $result, $message); + } + + /** + * Asserts that a string starts with a given prefix, ignoring differences in newlines. + * Helpful for doing cross platform tests of blocks of text. + * + * @param string $prefix The prefix to check for. + * @param string $string The string to search in. + * @param string $message The message to use for failure. + * @return void + */ + public function assertTextStartsWith($prefix, $string, $message = '') + { + $prefix = str_replace(["\r\n", "\r"], "\n", $prefix); + $string = str_replace(["\r\n", "\r"], "\n", $string); + $this->assertStringStartsWith($prefix, $string, $message); + } + + /** + * Asserts that a string starts not with a given prefix, ignoring differences in newlines. + * Helpful for doing cross platform tests of blocks of text. + * + * @param string $prefix The prefix to not find. + * @param string $string The string to search. + * @param string $message The message to use for failure. + * @return void + */ + public function assertTextStartsNotWith($prefix, $string, $message = '') + { + $prefix = str_replace(["\r\n", "\r"], "\n", $prefix); + $string = str_replace(["\r\n", "\r"], "\n", $string); + $this->assertStringStartsNotWith($prefix, $string, $message); + } + + /** + * Asserts that a string ends with a given prefix, ignoring differences in newlines. + * Helpful for doing cross platform tests of blocks of text. + * + * @param string $suffix The suffix to find. + * @param string $string The string to search. + * @param string $message The message to use for failure. + * @return void + */ + public function assertTextEndsWith($suffix, $string, $message = '') + { + $suffix = str_replace(["\r\n", "\r"], "\n", $suffix); + $string = str_replace(["\r\n", "\r"], "\n", $string); + $this->assertStringEndsWith($suffix, $string, $message); + } + + /** + * Asserts that a string ends not with a given prefix, ignoring differences in newlines. + * Helpful for doing cross platform tests of blocks of text. + * + * @param string $suffix The suffix to not find. + * @param string $string The string to search. + * @param string $message The message to use for failure. + * @return void + */ + public function assertTextEndsNotWith($suffix, $string, $message = '') + { + $suffix = str_replace(["\r\n", "\r"], "\n", $suffix); + $string = str_replace(["\r\n", "\r"], "\n", $string); + $this->assertStringEndsNotWith($suffix, $string, $message); + } + + /** + * Assert that a string contains another string, ignoring differences in newlines. + * Helpful for doing cross platform tests of blocks of text. + * + * @param string $needle The string to search for. + * @param string $haystack The string to search through. + * @param string $message The message to display on failure. + * @param bool $ignoreCase Whether or not the search should be case-sensitive. + * @return void + */ + public function assertTextContains($needle, $haystack, $message = '', $ignoreCase = false) + { + $needle = str_replace(["\r\n", "\r"], "\n", $needle); + $haystack = str_replace(["\r\n", "\r"], "\n", $haystack); + $this->assertContains($needle, $haystack, $message, $ignoreCase); + } + + /** + * Assert that a text doesn't contain another text, ignoring differences in newlines. + * Helpful for doing cross platform tests of blocks of text. + * + * @param string $needle The string to search for. + * @param string $haystack The string to search through. + * @param string $message The message to display on failure. + * @param bool $ignoreCase Whether or not the search should be case-sensitive. + * @return void + */ + public function assertTextNotContains($needle, $haystack, $message = '', $ignoreCase = false) + { + $needle = str_replace(["\r\n", "\r"], "\n", $needle); + $haystack = str_replace(["\r\n", "\r"], "\n", $haystack); + $this->assertNotContains($needle, $haystack, $message, $ignoreCase); + } + + /** + * Asserts HTML tags. + * + * @param string $string An HTML/XHTML/XML string + * @param array $expected An array, see above + * @param bool $fullDebug Whether or not more verbose output should be used. + * @return void + * @deprecated 3.0. Use assertHtml() instead. + */ + public function assertTags($string, $expected, $fullDebug = false) + { + deprecationWarning('TestCase::assertTags() is deprecated. Use TestCase::assertHtml() instead.'); + $this->assertHtml($expected, $string, $fullDebug); + } + + /** + * Asserts HTML tags. + * + * Takes an array $expected and generates a regex from it to match the provided $string. + * Samples for $expected: + * + * Checks for an input tag with a name attribute (contains any non-empty value) and an id + * attribute that contains 'my-input': + * + * ``` + * ['input' => ['name', 'id' => 'my-input']] + * ``` + * + * Checks for two p elements with some text in them: + * + * ``` + * [ + * ['p' => true], + * 'textA', + * '/p', + * ['p' => true], + * 'textB', + * '/p' + * ] + * ``` + * + * You can also specify a pattern expression as part of the attribute values, or the tag + * being defined, if you prepend the value with preg: and enclose it with slashes, like so: + * + * ``` + * [ + * ['input' => ['name', 'id' => 'preg:/FieldName\d+/']], + * 'preg:/My\s+field/' + * ] + * ``` + * + * Important: This function is very forgiving about whitespace and also accepts any + * permutation of attribute order. It will also allow whitespace between specified tags. + * + * @param array $expected An array, see above + * @param string $string An HTML/XHTML/XML string + * @param bool $fullDebug Whether or not more verbose output should be used. + * @return bool + */ + public function assertHtml($expected, $string, $fullDebug = false) + { + $regex = []; + $normalized = []; + foreach ((array)$expected as $key => $val) { + if (!is_numeric($key)) { + $normalized[] = [$key => $val]; + } else { + $normalized[] = $val; + } + } + $i = 0; + foreach ($normalized as $tags) { + if (!is_array($tags)) { + $tags = (string)$tags; + } + $i++; + if (is_string($tags) && $tags{0} === '<') { + $tags = [substr($tags, 1) => []]; + } elseif (is_string($tags)) { + $tagsTrimmed = preg_replace('/\s+/m', '', $tags); + + if (preg_match('/^\*?\//', $tags, $match) && $tagsTrimmed !== '//') { + $prefix = [null, null]; + + if ($match[0] === '*/') { + $prefix = ['Anything, ', '.*?']; + } + $regex[] = [ + sprintf('%sClose %s tag', $prefix[0], substr($tags, strlen($match[0]))), + sprintf('%s\s*<[\s]*\/[\s]*%s[\s]*>[\n\r]*', $prefix[1], substr($tags, strlen($match[0]))), + $i, + ]; + continue; + } + if (!empty($tags) && preg_match('/^preg\:\/(.+)\/$/i', $tags, $matches)) { + $tags = $matches[1]; + $type = 'Regex matches'; + } else { + $tags = '\s*' . preg_quote($tags, '/'); + $type = 'Text equals'; + } + $regex[] = [ + sprintf('%s "%s"', $type, $tags), + $tags, + $i, + ]; + continue; + } + foreach ($tags as $tag => $attributes) { + $regex[] = [ + sprintf('Open %s tag', $tag), + sprintf('[\s]*<%s', preg_quote($tag, '/')), + $i, + ]; + if ($attributes === true) { + $attributes = []; + } + $attrs = []; + $explanations = []; + $i = 1; + foreach ($attributes as $attr => $val) { + if (is_numeric($attr) && preg_match('/^preg\:\/(.+)\/$/i', $val, $matches)) { + $attrs[] = $matches[1]; + $explanations[] = sprintf('Regex "%s" matches', $matches[1]); + continue; + } + + $quotes = '["\']'; + if (is_numeric($attr)) { + $attr = $val; + $val = '.+?'; + $explanations[] = sprintf('Attribute "%s" present', $attr); + } elseif (!empty($val) && preg_match('/^preg\:\/(.+)\/$/i', $val, $matches)) { + $val = str_replace( + ['.*', '.+'], + ['.*?', '.+?'], + $matches[1] + ); + $quotes = $val !== $matches[1] ? '["\']' : '["\']?'; + + $explanations[] = sprintf('Attribute "%s" matches "%s"', $attr, $val); + } else { + $explanations[] = sprintf('Attribute "%s" == "%s"', $attr, $val); + $val = preg_quote($val, '/'); + } + $attrs[] = '[\s]+' . preg_quote($attr, '/') . '=' . $quotes . $val . $quotes; + $i++; + } + if ($attrs) { + $regex[] = [ + 'explains' => $explanations, + 'attrs' => $attrs, + ]; + } + $regex[] = [ + sprintf('End %s tag', $tag), + '[\s]*\/?[\s]*>[\n\r]*', + $i, + ]; + } + } + foreach ($regex as $i => $assertion) { + $matches = false; + if (isset($assertion['attrs'])) { + $string = $this->_assertAttributes($assertion, $string, $fullDebug, $regex); + if ($fullDebug === true && $string === false) { + debug($string, true); + debug($regex, true); + } + continue; + } + + list($description, $expressions, $itemNum) = $assertion; + $expression = null; + foreach ((array)$expressions as $expression) { + $expression = sprintf('/^%s/s', $expression); + if (preg_match($expression, $string, $match)) { + $matches = true; + $string = substr($string, strlen($match[0])); + break; + } + } + if (!$matches) { + if ($fullDebug === true) { + debug($string); + debug($regex); + } + $this->assertRegExp($expression, $string, sprintf('Item #%d / regex #%d failed: %s', $itemNum, $i, $description)); + + return false; + } + } + + $this->assertTrue(true, '%s'); + + return true; + } + + /** + * Check the attributes as part of an assertTags() check. + * + * @param array $assertions Assertions to run. + * @param string $string The HTML string to check. + * @param bool $fullDebug Whether or not more verbose output should be used. + * @param array|string $regex Full regexp from `assertHtml` + * @return string|bool + */ + protected function _assertAttributes($assertions, $string, $fullDebug = false, $regex = '') + { + $asserts = $assertions['attrs']; + $explains = $assertions['explains']; + do { + $matches = false; + $j = null; + foreach ($asserts as $j => $assert) { + if (preg_match(sprintf('/^%s/s', $assert), $string, $match)) { + $matches = true; + $string = substr($string, strlen($match[0])); + array_splice($asserts, $j, 1); + array_splice($explains, $j, 1); + break; + } + } + if ($matches === false) { + if ($fullDebug === true) { + debug($string); + debug($regex); + } + $this->assertTrue(false, 'Attribute did not match. Was expecting ' . $explains[$j]); + } + $len = count($asserts); + } while ($len > 0); + + return $string; + } + + /** + * Normalize a path for comparison. + * + * @param string $path Path separated by "/" slash. + * @return string Normalized path separated by DIRECTORY_SEPARATOR. + */ + protected function _normalizePath($path) + { + return str_replace('/', DIRECTORY_SEPARATOR, $path); + } + +// @codingStandardsIgnoreStart + + /** + * Compatibility function to test if a value is between an acceptable range. + * + * @param float $expected + * @param float $result + * @param float $margin the rage of acceptation + * @param string $message the text to display if the assertion is not correct + * @return void + */ + protected static function assertWithinRange($expected, $result, $margin, $message = '') + { + $upper = $result + $margin; + $lower = $result - $margin; + static::assertTrue(($expected <= $upper) && ($expected >= $lower), $message); + } + + /** + * Compatibility function to test if a value is not between an acceptable range. + * + * @param float $expected + * @param float $result + * @param float $margin the rage of acceptation + * @param string $message the text to display if the assertion is not correct + * @return void + */ + protected static function assertNotWithinRange($expected, $result, $margin, $message = '') + { + $upper = $result + $margin; + $lower = $result - $margin; + static::assertTrue(($expected > $upper) || ($expected < $lower), $message); + } + + /** + * Compatibility function to test paths. + * + * @param string $expected + * @param string $result + * @param string $message the text to display if the assertion is not correct + * @return void + */ + protected static function assertPathEquals($expected, $result, $message = '') + { + $expected = str_replace(DIRECTORY_SEPARATOR, '/', $expected); + $result = str_replace(DIRECTORY_SEPARATOR, '/', $result); + static::assertEquals($expected, $result, $message); + } + + /** + * Compatibility function for skipping. + * + * @param bool $condition Condition to trigger skipping + * @param string $message Message for skip + * @return bool + */ + protected function skipUnless($condition, $message = '') + { + if (!$condition) { + $this->markTestSkipped($message); + } + + return $condition; + } + +// @codingStandardsIgnoreEnd + + /** + * Mock a model, maintain fixtures and table association + * + * @param string $alias The model to get a mock for. + * @param array $methods The list of methods to mock + * @param array $options The config data for the mock's constructor. + * @throws \Cake\ORM\Exception\MissingTableClassException + * @return \Cake\ORM\Table|\PHPUnit_Framework_MockObject_MockObject + */ + public function getMockForModel($alias, array $methods = [], array $options = []) + { + /** @var \Cake\ORM\Table $className */ + $className = $this->_getTableClassName($alias, $options); + $connectionName = $className::defaultConnectionName(); + $connection = ConnectionManager::get($connectionName); + + $locator = $this->getTableLocator(); + + list(, $baseClass) = pluginSplit($alias); + $options += ['alias' => $baseClass, 'connection' => $connection]; + $options += $locator->getConfig($alias); + + /** @var \Cake\ORM\Table|\PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->getMockBuilder($className) + ->setMethods($methods) + ->setConstructorArgs([$options]) + ->getMock(); + + if (empty($options['entityClass']) && $mock->getEntityClass() === Entity::class) { + $parts = explode('\\', $className); + $entityAlias = Inflector::singularize(substr(array_pop($parts), 0, -5)); + $entityClass = implode('\\', array_slice($parts, 0, -1)) . '\\Entity\\' . $entityAlias; + if (class_exists($entityClass)) { + $mock->setEntityClass($entityClass); + } + } + + if (stripos($mock->getTable(), 'mock') === 0) { + $mock->setTable(Inflector::tableize($baseClass)); + } + + $locator->set($baseClass, $mock); + $locator->set($alias, $mock); + + return $mock; + } + + /** + * Gets the class name for the table. + * + * @param string $alias The model to get a mock for. + * @param array $options The config data for the mock's constructor. + * @return string + * @throws \Cake\ORM\Exception\MissingTableClassException + */ + protected function _getTableClassName($alias, array $options) + { + if (empty($options['className'])) { + $class = Inflector::camelize($alias); + $className = App::className($class, 'Model/Table', 'Table'); + if (!$className) { + throw new MissingTableClassException([$alias]); + } + $options['className'] = $className; + } + + return $options['className']; + } + + /** + * Set the app namespace + * + * @param string $appNamespace The app namespace, defaults to "TestApp". + * @return void + */ + public static function setAppNamespace($appNamespace = 'TestApp') + { + Configure::write('App.namespace', $appNamespace); + } +} diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/TestSuite.php b/app/vendor/cakephp/cakephp/src/TestSuite/TestSuite.php new file mode 100644 index 000000000..1c27a5bb2 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/TestSuite/TestSuite.php @@ -0,0 +1,65 @@ +read(true, true, true); + + foreach ($files as $file) { + if (substr($file, -4) === '.php') { + $this->addTestFile($file); + } + } + } + + /** + * Recursively adds all the files in a directory to the test suite. + * + * @param string $directory The directory subtree to add tests from. + * @return void + */ + public function addTestDirectoryRecursive($directory = '.') + { + $Folder = new Folder($directory); + $files = $Folder->tree(null, true, 'files'); + + foreach ($files as $file) { + if (substr($file, -4) === '.php') { + $this->addTestFile($file); + } + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Utility/CookieCryptTrait.php b/app/vendor/cakephp/cakephp/src/Utility/CookieCryptTrait.php new file mode 100644 index 000000000..77023b54e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Utility/CookieCryptTrait.php @@ -0,0 +1,182 @@ +_implode($value); + } + if ($encrypt === false) { + return $value; + } + $this->_checkCipher($encrypt); + $prefix = 'Q2FrZQ==.'; + $cipher = null; + if ($key === null) { + $key = $this->_getCookieEncryptionKey(); + } + if ($encrypt === 'rijndael') { + $cipher = Security::rijndael($value, $key, 'encrypt'); + } + if ($encrypt === 'aes') { + $cipher = Security::encrypt($value, $key); + } + + return $prefix . base64_encode($cipher); + } + + /** + * Helper method for validating encryption cipher names. + * + * @param string $encrypt The cipher name. + * @return void + * @throws \RuntimeException When an invalid cipher is provided. + */ + protected function _checkCipher($encrypt) + { + if (!in_array($encrypt, $this->_validCiphers)) { + $msg = sprintf( + 'Invalid encryption cipher. Must be one of %s.', + implode(', ', $this->_validCiphers) + ); + throw new RuntimeException($msg); + } + } + + /** + * Decrypts $value using public $type method in Security class + * + * @param array $values Values to decrypt + * @param string|bool $mode Encryption mode + * @param string|null $key Used as the security salt if specified. + * @return string|array Decrypted values + */ + protected function _decrypt($values, $mode, $key = null) + { + if (is_string($values)) { + return $this->_decode($values, $mode, $key); + } + + $decrypted = []; + foreach ($values as $name => $value) { + $decrypted[$name] = $this->_decode($value, $mode, $key); + } + + return $decrypted; + } + + /** + * Decodes and decrypts a single value. + * + * @param string $value The value to decode & decrypt. + * @param string|false $encrypt The encryption cipher to use. + * @param string|null $key Used as the security salt if specified. + * @return string|array Decoded values. + */ + protected function _decode($value, $encrypt, $key) + { + if (!$encrypt) { + return $this->_explode($value); + } + $this->_checkCipher($encrypt); + $prefix = 'Q2FrZQ==.'; + $value = base64_decode(substr($value, strlen($prefix))); + if ($key === null) { + $key = $this->_getCookieEncryptionKey(); + } + if ($encrypt === 'rijndael') { + $value = Security::rijndael($value, $key, 'decrypt'); + } + if ($encrypt === 'aes') { + $value = Security::decrypt($value, $key); + } + + return $this->_explode($value); + } + + /** + * Implode method to keep keys are multidimensional arrays + * + * @param array $array Map of key and values + * @return string A json encoded string. + */ + protected function _implode(array $array) + { + return json_encode($array); + } + + /** + * Explode method to return array from string set in CookieComponent::_implode() + * Maintains reading backwards compatibility with 1.x CookieComponent::_implode(). + * + * @param string $string A string containing JSON encoded data, or a bare string. + * @return string|array Map of key and values + */ + protected function _explode($string) + { + $first = substr($string, 0, 1); + if ($first === '{' || $first === '[') { + $ret = json_decode($string, true); + + return ($ret !== null) ? $ret : $string; + } + $array = []; + foreach (explode(',', $string) as $pair) { + $key = explode('|', $pair); + if (!isset($key[1])) { + return $key[0]; + } + $array[$key[0]] = $key[1]; + } + + return $array; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Utility/Crypto/Mcrypt.php b/app/vendor/cakephp/cakephp/src/Utility/Crypto/Mcrypt.php new file mode 100644 index 000000000..b67c72e91 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Utility/Crypto/Mcrypt.php @@ -0,0 +1,125 @@ +`, `<`, `>=`, `<=` Value comparison. + * - `=/.../` Regular expression pattern match. + * + * Given a set of User array data, from a `$User->find('all')` call: + * + * - `1.User.name` Get the name of the user at index 1. + * - `{n}.User.name` Get the name of every user in the set of users. + * - `{n}.User[id].name` Get the name of every user with an id key. + * - `{n}.User[id>=2].name` Get the name of every user with an id key greater than or equal to 2. + * - `{n}.User[username=/^paul/]` Get User elements with username matching `^paul`. + * - `{n}.User[id=1].name` Get the Users name with id matching `1`. + * + * @param array|\ArrayAccess $data The data to extract from. + * @param string $path The path to extract. + * @return array|\ArrayAccess An array of the extracted values. Returns an empty array + * if there are no matches. + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::extract + */ + public static function extract($data, $path) + { + if (!(is_array($data) || $data instanceof ArrayAccess)) { + throw new InvalidArgumentException( + 'Invalid data type, must be an array or \ArrayAccess instance.' + ); + } + + if (empty($path)) { + return $data; + } + + // Simple paths. + if (!preg_match('/[{\[]/', $path)) { + $data = static::get($data, $path); + if ($data !== null && !(is_array($data) || $data instanceof ArrayAccess)) { + return [$data]; + } + + return $data !== null ? (array)$data : []; + } + + if (strpos($path, '[') === false) { + $tokens = explode('.', $path); + } else { + $tokens = Text::tokenize($path, '.', '[', ']'); + } + + $_key = '__set_item__'; + + $context = [$_key => [$data]]; + + foreach ($tokens as $token) { + $next = []; + + list($token, $conditions) = self::_splitConditions($token); + + foreach ($context[$_key] as $item) { + if (is_object($item) && method_exists($item, 'toArray')) { + /** @var \Cake\Datasource\EntityInterface $item */ + $item = $item->toArray(); + } + foreach ((array)$item as $k => $v) { + if (static::_matchToken($k, $token)) { + $next[] = $v; + } + } + } + + // Filter for attributes. + if ($conditions) { + $filter = []; + foreach ($next as $item) { + if ((is_array($item) || $item instanceof ArrayAccess) && + static::_matches($item, $conditions) + ) { + $filter[] = $item; + } + } + $next = $filter; + } + $context = [$_key => $next]; + } + + return $context[$_key]; + } + + /** + * Split token conditions + * + * @param string $token the token being splitted. + * @return array [token, conditions] with token splitted + */ + protected static function _splitConditions($token) + { + $conditions = false; + $position = strpos($token, '['); + if ($position !== false) { + $conditions = substr($token, $position); + $token = substr($token, 0, $position); + } + + return [$token, $conditions]; + } + + /** + * Check a key against a token. + * + * @param string $key The key in the array being searched. + * @param string $token The token being matched. + * @return bool + */ + protected static function _matchToken($key, $token) + { + switch ($token) { + case '{n}': + return is_numeric($key); + case '{s}': + return is_string($key); + case '{*}': + return true; + default: + return is_numeric($token) ? ($key == $token) : $key === $token; + } + } + + /** + * Checks whether or not $data matches the attribute patterns + * + * @param array|\ArrayAccess $data Array of data to match. + * @param string $selector The patterns to match. + * @return bool Fitness of expression. + */ + protected static function _matches($data, $selector) + { + preg_match_all( + '/(\[ (?P[^=>[><]) \s* (?P(?:\/.*?\/ | [^\]]+)) )? \])/x', + $selector, + $conditions, + PREG_SET_ORDER + ); + + foreach ($conditions as $cond) { + $attr = $cond['attr']; + $op = isset($cond['op']) ? $cond['op'] : null; + $val = isset($cond['val']) ? $cond['val'] : null; + + // Presence test. + if (empty($op) && empty($val) && !isset($data[$attr])) { + return false; + } + + // Empty attribute = fail. + if (!(isset($data[$attr]) || array_key_exists($attr, $data))) { + return false; + } + + $prop = null; + if (isset($data[$attr])) { + $prop = $data[$attr]; + } + $isBool = is_bool($prop); + if ($isBool && is_numeric($val)) { + $prop = $prop ? '1' : '0'; + } elseif ($isBool) { + $prop = $prop ? 'true' : 'false'; + } elseif (is_numeric($prop)) { + $prop = (string)$prop; + } + + // Pattern matches and other operators. + if ($op === '=' && $val && $val[0] === '/') { + if (!preg_match($val, $prop)) { + return false; + } + } elseif (($op === '=' && $prop != $val) || + ($op === '!=' && $prop == $val) || + ($op === '>' && $prop <= $val) || + ($op === '<' && $prop >= $val) || + ($op === '>=' && $prop < $val) || + ($op === '<=' && $prop > $val) + ) { + return false; + } + } + + return true; + } + + /** + * Insert $values into an array with the given $path. You can use + * `{n}` and `{s}` elements to insert $data multiple times. + * + * @param array $data The data to insert into. + * @param string $path The path to insert at. + * @param array|null $values The values to insert. + * @return array The data with $values inserted. + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::insert + */ + public static function insert(array $data, $path, $values = null) + { + $noTokens = strpos($path, '[') === false; + if ($noTokens && strpos($path, '.') === false) { + $data[$path] = $values; + + return $data; + } + + if ($noTokens) { + $tokens = explode('.', $path); + } else { + $tokens = Text::tokenize($path, '.', '[', ']'); + } + + if ($noTokens && strpos($path, '{') === false) { + return static::_simpleOp('insert', $data, $tokens, $values); + } + + $token = array_shift($tokens); + $nextPath = implode('.', $tokens); + + list($token, $conditions) = static::_splitConditions($token); + + foreach ($data as $k => $v) { + if (static::_matchToken($k, $token)) { + if (!$conditions || static::_matches($v, $conditions)) { + $data[$k] = $nextPath + ? static::insert($v, $nextPath, $values) + : array_merge($v, (array)$values); + } + } + } + + return $data; + } + + /** + * Perform a simple insert/remove operation. + * + * @param string $op The operation to do. + * @param array $data The data to operate on. + * @param array $path The path to work on. + * @param mixed $values The values to insert when doing inserts. + * @return array data. + */ + protected static function _simpleOp($op, $data, $path, $values = null) + { + $_list =& $data; + + $count = count($path); + $last = $count - 1; + foreach ($path as $i => $key) { + if ($op === 'insert') { + if ($i === $last) { + $_list[$key] = $values; + + return $data; + } + if (!isset($_list[$key])) { + $_list[$key] = []; + } + $_list =& $_list[$key]; + if (!is_array($_list)) { + $_list = []; + } + } elseif ($op === 'remove') { + if ($i === $last) { + if (is_array($_list)) { + unset($_list[$key]); + } + + return $data; + } + if (!isset($_list[$key])) { + return $data; + } + $_list =& $_list[$key]; + } + } + } + + /** + * Remove data matching $path from the $data array. + * You can use `{n}` and `{s}` to remove multiple elements + * from $data. + * + * @param array $data The data to operate on + * @param string $path A path expression to use to remove. + * @return array The modified array. + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::remove + */ + public static function remove(array $data, $path) + { + $noTokens = strpos($path, '[') === false; + $noExpansion = strpos($path, '{') === false; + + if ($noExpansion && $noTokens && strpos($path, '.') === false) { + unset($data[$path]); + + return $data; + } + + $tokens = $noTokens ? explode('.', $path) : Text::tokenize($path, '.', '[', ']'); + + if ($noExpansion && $noTokens) { + return static::_simpleOp('remove', $data, $tokens); + } + + $token = array_shift($tokens); + $nextPath = implode('.', $tokens); + + list($token, $conditions) = self::_splitConditions($token); + + foreach ($data as $k => $v) { + $match = static::_matchToken($k, $token); + if ($match && is_array($v)) { + if ($conditions) { + if (static::_matches($v, $conditions)) { + if ($nextPath !== '') { + $data[$k] = static::remove($v, $nextPath); + } else { + unset($data[$k]); + } + } + } else { + $data[$k] = static::remove($v, $nextPath); + } + if (empty($data[$k])) { + unset($data[$k]); + } + } elseif ($match && $nextPath === '') { + unset($data[$k]); + } + } + + return $data; + } + + /** + * Creates an associative array using `$keyPath` as the path to build its keys, and optionally + * `$valuePath` as path to get the values. If `$valuePath` is not specified, all values will be initialized + * to null (useful for Hash::merge). You can optionally group the values by what is obtained when + * following the path specified in `$groupPath`. + * + * @param array $data Array from where to extract keys and values + * @param string $keyPath A dot-separated string. + * @param string|null $valuePath A dot-separated string. + * @param string|null $groupPath A dot-separated string. + * @return array Combined array + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::combine + * @throws \RuntimeException When keys and values count is unequal. + */ + public static function combine(array $data, $keyPath, $valuePath = null, $groupPath = null) + { + if (empty($data)) { + return []; + } + + if (is_array($keyPath)) { + $format = array_shift($keyPath); + $keys = static::format($data, $keyPath, $format); + } else { + $keys = static::extract($data, $keyPath); + } + if (empty($keys)) { + return []; + } + + $vals = null; + if (!empty($valuePath) && is_array($valuePath)) { + $format = array_shift($valuePath); + $vals = static::format($data, $valuePath, $format); + } elseif (!empty($valuePath)) { + $vals = static::extract($data, $valuePath); + } + if (empty($vals)) { + $vals = array_fill(0, count($keys), null); + } + + if (count($keys) !== count($vals)) { + throw new RuntimeException( + 'Hash::combine() needs an equal number of keys + values.' + ); + } + + if ($groupPath !== null) { + $group = static::extract($data, $groupPath); + if (!empty($group)) { + $c = count($keys); + $out = []; + for ($i = 0; $i < $c; $i++) { + if (!isset($group[$i])) { + $group[$i] = 0; + } + if (!isset($out[$group[$i]])) { + $out[$group[$i]] = []; + } + $out[$group[$i]][$keys[$i]] = $vals[$i]; + } + + return $out; + } + } + if (empty($vals)) { + return []; + } + + return array_combine($keys, $vals); + } + + /** + * Returns a formatted series of values extracted from `$data`, using + * `$format` as the format and `$paths` as the values to extract. + * + * Usage: + * + * ``` + * $result = Hash::format($users, ['{n}.User.id', '{n}.User.name'], '%s : %s'); + * ``` + * + * The `$format` string can use any format options that `vsprintf()` and `sprintf()` do. + * + * @param array $data Source array from which to extract the data + * @param array $paths An array containing one or more Hash::extract()-style key paths + * @param string $format Format string into which values will be inserted, see sprintf() + * @return array|null An array of strings extracted from `$path` and formatted with `$format` + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::format + * @see sprintf() + * @see \Cake\Utility\Hash::extract() + */ + public static function format(array $data, array $paths, $format) + { + $extracted = []; + $count = count($paths); + + if (!$count) { + return null; + } + + for ($i = 0; $i < $count; $i++) { + $extracted[] = static::extract($data, $paths[$i]); + } + $out = []; + $data = $extracted; + $count = count($data[0]); + + $countTwo = count($data); + for ($j = 0; $j < $count; $j++) { + $args = []; + for ($i = 0; $i < $countTwo; $i++) { + if (array_key_exists($j, $data[$i])) { + $args[] = $data[$i][$j]; + } + } + $out[] = vsprintf($format, $args); + } + + return $out; + } + + /** + * Determines if one array contains the exact keys and values of another. + * + * @param array $data The data to search through. + * @param array $needle The values to file in $data + * @return bool true If $data contains $needle, false otherwise + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::contains + */ + public static function contains(array $data, array $needle) + { + if (empty($data) || empty($needle)) { + return false; + } + $stack = []; + + while (!empty($needle)) { + $key = key($needle); + $val = $needle[$key]; + unset($needle[$key]); + + if (array_key_exists($key, $data) && is_array($val)) { + $next = $data[$key]; + unset($data[$key]); + + if (!empty($val)) { + $stack[] = [$val, $next]; + } + } elseif (!array_key_exists($key, $data) || $data[$key] != $val) { + return false; + } + + if (empty($needle) && !empty($stack)) { + list($needle, $data) = array_pop($stack); + } + } + + return true; + } + + /** + * Test whether or not a given path exists in $data. + * This method uses the same path syntax as Hash::extract() + * + * Checking for paths that could target more than one element will + * make sure that at least one matching element exists. + * + * @param array $data The data to check. + * @param string $path The path to check for. + * @return bool Existence of path. + * @see \Cake\Utility\Hash::extract() + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::check + */ + public static function check(array $data, $path) + { + $results = static::extract($data, $path); + if (!is_array($results)) { + return false; + } + + return count($results) > 0; + } + + /** + * Recursively filters a data set. + * + * @param array $data Either an array to filter, or value when in callback + * @param callable|array $callback A function to filter the data with. Defaults to + * `static::_filter()` Which strips out all non-zero empty values. + * @return array Filtered array + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::filter + */ + public static function filter(array $data, $callback = ['self', '_filter']) + { + foreach ($data as $k => $v) { + if (is_array($v)) { + $data[$k] = static::filter($v, $callback); + } + } + + return array_filter($data, $callback); + } + + /** + * Callback function for filtering. + * + * @param mixed $var Array to filter. + * @return bool + */ + protected static function _filter($var) + { + return $var === 0 || $var === 0.0 || $var === '0' || !empty($var); + } + + /** + * Collapses a multi-dimensional array into a single dimension, using a delimited array path for + * each array element's key, i.e. [['Foo' => ['Bar' => 'Far']]] becomes + * ['0.Foo.Bar' => 'Far'].) + * + * @param array $data Array to flatten + * @param string $separator String used to separate array key elements in a path, defaults to '.' + * @return array + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::flatten + */ + public static function flatten(array $data, $separator = '.') + { + $result = []; + $stack = []; + $path = null; + + reset($data); + while (!empty($data)) { + $key = key($data); + $element = $data[$key]; + unset($data[$key]); + + if (is_array($element) && !empty($element)) { + if (!empty($data)) { + $stack[] = [$data, $path]; + } + $data = $element; + reset($data); + $path .= $key . $separator; + } else { + $result[$path . $key] = $element; + } + + if (empty($data) && !empty($stack)) { + list($data, $path) = array_pop($stack); + reset($data); + } + } + + return $result; + } + + /** + * Expands a flat array to a nested array. + * + * For example, unflattens an array that was collapsed with `Hash::flatten()` + * into a multi-dimensional array. So, `['0.Foo.Bar' => 'Far']` becomes + * `[['Foo' => ['Bar' => 'Far']]]`. + * + * @param array $data Flattened array + * @param string $separator The delimiter used + * @return array + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::expand + */ + public static function expand(array $data, $separator = '.') + { + $result = []; + foreach ($data as $flat => $value) { + $keys = explode($separator, $flat); + $keys = array_reverse($keys); + $child = [ + $keys[0] => $value + ]; + array_shift($keys); + foreach ($keys as $k) { + $child = [ + $k => $child + ]; + } + + $stack = [[$child, &$result]]; + static::_merge($stack, $result); + } + + return $result; + } + + /** + * This function can be thought of as a hybrid between PHP's `array_merge` and `array_merge_recursive`. + * + * The difference between this method and the built-in ones, is that if an array key contains another array, then + * Hash::merge() will behave in a recursive fashion (unlike `array_merge`). But it will not act recursively for + * keys that contain scalar values (unlike `array_merge_recursive`). + * + * Note: This function will work with an unlimited amount of arguments and typecasts non-array parameters into arrays. + * + * @param array $data Array to be merged + * @param mixed $merge Array to merge with. The argument and all trailing arguments will be array cast when merged + * @return array Merged array + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::merge + */ + public static function merge(array $data, $merge) + { + $args = array_slice(func_get_args(), 1); + $return = $data; + $stack = []; + + foreach ($args as &$curArg) { + $stack[] = [(array)$curArg, &$return]; + } + unset($curArg); + static::_merge($stack, $return); + + return $return; + } + + /** + * Merge helper function to reduce duplicated code between merge() and expand(). + * + * @param array $stack The stack of operations to work with. + * @param array $return The return value to operate on. + * @return void + */ + protected static function _merge($stack, &$return) + { + while (!empty($stack)) { + foreach ($stack as $curKey => &$curMerge) { + foreach ($curMerge[0] as $key => &$val) { + $isArray = is_array($curMerge[1]); + if ($isArray && !empty($curMerge[1][$key]) && (array)$curMerge[1][$key] === $curMerge[1][$key] && (array)$val === $val) { + // Recurse into the current merge data as it is an array. + $stack[] = [&$val, &$curMerge[1][$key]]; + } elseif ((int)$key === $key && $isArray && isset($curMerge[1][$key])) { + $curMerge[1][] = $val; + } else { + $curMerge[1][$key] = $val; + } + } + unset($stack[$curKey]); + } + unset($curMerge); + } + } + + /** + * Checks to see if all the values in the array are numeric + * + * @param array $data The array to check. + * @return bool true if values are numeric, false otherwise + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::numeric + */ + public static function numeric(array $data) + { + if (empty($data)) { + return false; + } + + return $data === array_filter($data, 'is_numeric'); + } + + /** + * Counts the dimensions of an array. + * Only considers the dimension of the first element in the array. + * + * If you have an un-even or heterogeneous array, consider using Hash::maxDimensions() + * to get the dimensions of the array. + * + * @param array $data Array to count dimensions on + * @return int The number of dimensions in $data + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::dimensions + */ + public static function dimensions(array $data) + { + if (empty($data)) { + return 0; + } + reset($data); + $depth = 1; + while ($elem = array_shift($data)) { + if (is_array($elem)) { + $depth++; + $data = $elem; + } else { + break; + } + } + + return $depth; + } + + /** + * Counts the dimensions of *all* array elements. Useful for finding the maximum + * number of dimensions in a mixed array. + * + * @param array $data Array to count dimensions on + * @return int The maximum number of dimensions in $data + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::maxDimensions + */ + public static function maxDimensions(array $data) + { + $depth = []; + if (is_array($data) && !empty($data)) { + foreach ($data as $value) { + if (is_array($value)) { + $depth[] = static::maxDimensions($value) + 1; + } else { + $depth[] = 1; + } + } + } + + return empty($depth) ? 0 : max($depth); + } + + /** + * Map a callback across all elements in a set. + * Can be provided a path to only modify slices of the set. + * + * @param array $data The data to map over, and extract data out of. + * @param string $path The path to extract for mapping over. + * @param callable $function The function to call on each extracted value. + * @return array An array of the modified values. + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::map + */ + public static function map(array $data, $path, $function) + { + $values = (array)static::extract($data, $path); + + return array_map($function, $values); + } + + /** + * Reduce a set of extracted values using `$function`. + * + * @param array $data The data to reduce. + * @param string $path The path to extract from $data. + * @param callable $function The function to call on each extracted value. + * @return mixed The reduced value. + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::reduce + */ + public static function reduce(array $data, $path, $function) + { + $values = (array)static::extract($data, $path); + + return array_reduce($values, $function); + } + + /** + * Apply a callback to a set of extracted values using `$function`. + * The function will get the extracted values as the first argument. + * + * ### Example + * + * You can easily count the results of an extract using apply(). + * For example to count the comments on an Article: + * + * ``` + * $count = Hash::apply($data, 'Article.Comment.{n}', 'count'); + * ``` + * + * You could also use a function like `array_sum` to sum the results. + * + * ``` + * $total = Hash::apply($data, '{n}.Item.price', 'array_sum'); + * ``` + * + * @param array $data The data to reduce. + * @param string $path The path to extract from $data. + * @param callable $function The function to call on each extracted value. + * @return mixed The results of the applied method. + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::apply + */ + public static function apply(array $data, $path, $function) + { + $values = (array)static::extract($data, $path); + + return call_user_func($function, $values); + } + + /** + * Sorts an array by any value, determined by a Set-compatible path + * + * ### Sort directions + * + * - `asc` Sort ascending. + * - `desc` Sort descending. + * + * ### Sort types + * + * - `regular` For regular sorting (don't change types) + * - `numeric` Compare values numerically + * - `string` Compare values as strings + * - `locale` Compare items as strings, based on the current locale + * - `natural` Compare items as strings using "natural ordering" in a human friendly way + * Will sort foo10 below foo2 as an example. + * + * To do case insensitive sorting, pass the type as an array as follows: + * + * ``` + * Hash::sort($data, 'some.attribute', 'asc', ['type' => 'regular', 'ignoreCase' => true]); + * ``` + * + * When using the array form, `type` defaults to 'regular'. The `ignoreCase` option + * defaults to `false`. + * + * @param array $data An array of data to sort + * @param string $path A Set-compatible path to the array value + * @param string $dir See directions above. Defaults to 'asc'. + * @param array|string $type See direction types above. Defaults to 'regular'. + * @return array Sorted array of data + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::sort + */ + public static function sort(array $data, $path, $dir = 'asc', $type = 'regular') + { + if (empty($data)) { + return []; + } + $originalKeys = array_keys($data); + $numeric = is_numeric(implode('', $originalKeys)); + if ($numeric) { + $data = array_values($data); + } + $sortValues = static::extract($data, $path); + $dataCount = count($data); + + // Make sortValues match the data length, as some keys could be missing + // the sorted value path. + $missingData = count($sortValues) < $dataCount; + if ($missingData && $numeric) { + // Get the path without the leading '{n}.' + $itemPath = substr($path, 4); + foreach ($data as $key => $value) { + $sortValues[$key] = static::get($value, $itemPath); + } + } elseif ($missingData) { + $sortValues = array_pad($sortValues, $dataCount, null); + } + $result = static::_squash($sortValues); + $keys = static::extract($result, '{n}.id'); + $values = static::extract($result, '{n}.value'); + + $dir = strtolower($dir); + $ignoreCase = false; + + // $type can be overloaded for case insensitive sort + if (is_array($type)) { + $type += ['ignoreCase' => false, 'type' => 'regular']; + $ignoreCase = $type['ignoreCase']; + $type = $type['type']; + } + $type = strtolower($type); + + if ($dir === 'asc') { + $dir = SORT_ASC; + } else { + $dir = SORT_DESC; + } + if ($type === 'numeric') { + $type = \SORT_NUMERIC; + } elseif ($type === 'string') { + $type = \SORT_STRING; + } elseif ($type === 'natural') { + $type = \SORT_NATURAL; + } elseif ($type === 'locale') { + $type = \SORT_LOCALE_STRING; + } else { + $type = \SORT_REGULAR; + } + if ($ignoreCase) { + $values = array_map('mb_strtolower', $values); + } + array_multisort($values, $dir, $type, $keys, $dir, $type); + $sorted = []; + $keys = array_unique($keys); + + foreach ($keys as $k) { + if ($numeric) { + $sorted[] = $data[$k]; + continue; + } + if (isset($originalKeys[$k])) { + $sorted[$originalKeys[$k]] = $data[$originalKeys[$k]]; + } else { + $sorted[$k] = $data[$k]; + } + } + + return $sorted; + } + + /** + * Helper method for sort() + * Squashes an array to a single hash so it can be sorted. + * + * @param array $data The data to squash. + * @param string|null $key The key for the data. + * @return array + */ + protected static function _squash(array $data, $key = null) + { + $stack = []; + foreach ($data as $k => $r) { + $id = $k; + if ($key !== null) { + $id = $key; + } + if (is_array($r) && !empty($r)) { + $stack = array_merge($stack, static::_squash($r, $id)); + } else { + $stack[] = ['id' => $id, 'value' => $r]; + } + } + + return $stack; + } + + /** + * Computes the difference between two complex arrays. + * This method differs from the built-in array_diff() in that it will preserve keys + * and work on multi-dimensional arrays. + * + * @param array $data First value + * @param array $compare Second value + * @return array Returns the key => value pairs that are not common in $data and $compare + * The expression for this function is ($data - $compare) + ($compare - ($data - $compare)) + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::diff + */ + public static function diff(array $data, array $compare) + { + if (empty($data)) { + return (array)$compare; + } + if (empty($compare)) { + return (array)$data; + } + $intersection = array_intersect_key($data, $compare); + while (($key = key($intersection)) !== null) { + if ($data[$key] == $compare[$key]) { + unset($data[$key], $compare[$key]); + } + next($intersection); + } + + return $data + $compare; + } + + /** + * Merges the difference between $data and $compare onto $data. + * + * @param array $data The data to append onto. + * @param array $compare The data to compare and append onto. + * @return array The merged array. + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::mergeDiff + */ + public static function mergeDiff(array $data, array $compare) + { + if (empty($data) && !empty($compare)) { + return $compare; + } + if (empty($compare)) { + return $data; + } + foreach ($compare as $key => $value) { + if (!array_key_exists($key, $data)) { + $data[$key] = $value; + } elseif (is_array($value)) { + $data[$key] = static::mergeDiff($data[$key], $compare[$key]); + } + } + + return $data; + } + + /** + * Normalizes an array, and converts it to a standard format. + * + * @param array $data List to normalize + * @param bool $assoc If true, $data will be converted to an associative array. + * @return array + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::normalize + */ + public static function normalize(array $data, $assoc = true) + { + $keys = array_keys($data); + $count = count($keys); + $numeric = true; + + if (!$assoc) { + for ($i = 0; $i < $count; $i++) { + if (!is_int($keys[$i])) { + $numeric = false; + break; + } + } + } + if (!$numeric || $assoc) { + $newList = []; + for ($i = 0; $i < $count; $i++) { + if (is_int($keys[$i])) { + $newList[$data[$keys[$i]]] = null; + } else { + $newList[$keys[$i]] = $data[$keys[$i]]; + } + } + $data = $newList; + } + + return $data; + } + + /** + * Takes in a flat array and returns a nested array + * + * ### Options: + * + * - `children` The key name to use in the resultset for children. + * - `idPath` The path to a key that identifies each entry. Should be + * compatible with Hash::extract(). Defaults to `{n}.$alias.id` + * - `parentPath` The path to a key that identifies the parent of each entry. + * Should be compatible with Hash::extract(). Defaults to `{n}.$alias.parent_id` + * - `root` The id of the desired top-most result. + * + * @param array $data The data to nest. + * @param array $options Options are: + * @return array of results, nested + * @see \Cake\Utility\Hash::extract() + * @throws \InvalidArgumentException When providing invalid data. + * @link https://book.cakephp.org/3.0/en/core-libraries/hash.html#Cake\Utility\Hash::nest + */ + public static function nest(array $data, array $options = []) + { + if (!$data) { + return $data; + } + + $alias = key(current($data)); + $options += [ + 'idPath' => "{n}.$alias.id", + 'parentPath' => "{n}.$alias.parent_id", + 'children' => 'children', + 'root' => null + ]; + + $return = $idMap = []; + $ids = static::extract($data, $options['idPath']); + + $idKeys = explode('.', $options['idPath']); + array_shift($idKeys); + + $parentKeys = explode('.', $options['parentPath']); + array_shift($parentKeys); + + foreach ($data as $result) { + $result[$options['children']] = []; + + $id = static::get($result, $idKeys); + $parentId = static::get($result, $parentKeys); + + if (isset($idMap[$id][$options['children']])) { + $idMap[$id] = array_merge($result, (array)$idMap[$id]); + } else { + $idMap[$id] = array_merge($result, [$options['children'] => []]); + } + if (!$parentId || !in_array($parentId, $ids)) { + $return[] =& $idMap[$id]; + } else { + $idMap[$parentId][$options['children']][] =& $idMap[$id]; + } + } + + if (!$return) { + throw new InvalidArgumentException('Invalid data array to nest.'); + } + + if ($options['root']) { + $root = $options['root']; + } else { + $root = static::get($return[0], $parentKeys); + } + + foreach ($return as $i => $result) { + $id = static::get($result, $idKeys); + $parentId = static::get($result, $parentKeys); + if ($id !== $root && $parentId != $root) { + unset($return[$i]); + } + } + + return array_values($return); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Utility/Inflector.php b/app/vendor/cakephp/cakephp/src/Utility/Inflector.php new file mode 100644 index 000000000..c970f48e6 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Utility/Inflector.php @@ -0,0 +1,774 @@ + '\1tatuses', + '/(quiz)$/i' => '\1zes', + '/^(ox)$/i' => '\1\2en', + '/([m|l])ouse$/i' => '\1ice', + '/(matr|vert|ind)(ix|ex)$/i' => '\1ices', + '/(x|ch|ss|sh)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(hive)$/i' => '\1s', + '/(chef)$/i' => '\1s', + '/(?:([^f])fe|([lre])f)$/i' => '\1\2ves', + '/sis$/i' => 'ses', + '/([ti])um$/i' => '\1a', + '/(p)erson$/i' => '\1eople', + '/(? '\1en', + '/(c)hild$/i' => '\1hildren', + '/(buffal|tomat)o$/i' => '\1\2oes', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin)us$/i' => '\1i', + '/us$/i' => 'uses', + '/(alias)$/i' => '\1es', + '/(ax|cris|test)is$/i' => '\1es', + '/s$/' => 's', + '/^$/' => '', + '/$/' => 's', + ]; + + /** + * Singular inflector rules + * + * @var array + */ + protected static $_singular = [ + '/(s)tatuses$/i' => '\1\2tatus', + '/^(.*)(menu)s$/i' => '\1\2', + '/(quiz)zes$/i' => '\\1', + '/(matr)ices$/i' => '\1ix', + '/(vert|ind)ices$/i' => '\1ex', + '/^(ox)en/i' => '\1', + '/(alias)(es)*$/i' => '\1', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us', + '/([ftw]ax)es/i' => '\1', + '/(cris|ax|test)es$/i' => '\1is', + '/(shoe)s$/i' => '\1', + '/(o)es$/i' => '\1', + '/ouses$/' => 'ouse', + '/([^a])uses$/' => '\1us', + '/([m|l])ice$/i' => '\1ouse', + '/(x|ch|ss|sh)es$/i' => '\1', + '/(m)ovies$/i' => '\1\2ovie', + '/(s)eries$/i' => '\1\2eries', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/(tive)s$/i' => '\1', + '/(hive)s$/i' => '\1', + '/(drive)s$/i' => '\1', + '/([le])ves$/i' => '\1f', + '/([^rfoa])ves$/i' => '\1fe', + '/(^analy)ses$/i' => '\1sis', + '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', + '/([ti])a$/i' => '\1um', + '/(p)eople$/i' => '\1\2erson', + '/(m)en$/i' => '\1an', + '/(c)hildren$/i' => '\1\2hild', + '/(n)ews$/i' => '\1\2ews', + '/eaus$/' => 'eau', + '/^(.*us)$/' => '\\1', + '/s$/i' => '' + ]; + + /** + * Irregular rules + * + * @var array + */ + protected static $_irregular = [ + 'atlas' => 'atlases', + 'beef' => 'beefs', + 'brief' => 'briefs', + 'brother' => 'brothers', + 'cafe' => 'cafes', + 'child' => 'children', + 'cookie' => 'cookies', + 'corpus' => 'corpuses', + 'cow' => 'cows', + 'criterion' => 'criteria', + 'ganglion' => 'ganglions', + 'genie' => 'genies', + 'genus' => 'genera', + 'graffito' => 'graffiti', + 'hoof' => 'hoofs', + 'loaf' => 'loaves', + 'man' => 'men', + 'money' => 'monies', + 'mongoose' => 'mongooses', + 'move' => 'moves', + 'mythos' => 'mythoi', + 'niche' => 'niches', + 'numen' => 'numina', + 'occiput' => 'occiputs', + 'octopus' => 'octopuses', + 'opus' => 'opuses', + 'ox' => 'oxen', + 'penis' => 'penises', + 'person' => 'people', + 'sex' => 'sexes', + 'soliloquy' => 'soliloquies', + 'testis' => 'testes', + 'trilby' => 'trilbys', + 'turf' => 'turfs', + 'potato' => 'potatoes', + 'hero' => 'heroes', + 'tooth' => 'teeth', + 'goose' => 'geese', + 'foot' => 'feet', + 'foe' => 'foes', + 'sieve' => 'sieves', + 'cache' => 'caches', + ]; + + /** + * Words that should not be inflected + * + * @var array + */ + protected static $_uninflected = [ + '.*[nrlm]ese', '.*data', '.*deer', '.*fish', '.*measles', '.*ois', + '.*pox', '.*sheep', 'people', 'feedback', 'stadia', '.*?media', + 'chassis', 'clippers', 'debris', 'diabetes', 'equipment', 'gallows', + 'graffiti', 'headquarters', 'information', 'innings', 'news', 'nexus', + 'pokemon', 'proceedings', 'research', 'sea[- ]bass', 'series', 'species', 'weather' + ]; + + /** + * Default map of accented and special characters to ASCII characters + * + * @var array + */ + protected static $_transliteration = [ + 'ä' => 'ae', + 'æ' => 'ae', + 'ǽ' => 'ae', + 'ö' => 'oe', + 'œ' => 'oe', + 'ü' => 'ue', + 'Ä' => 'Ae', + 'Ü' => 'Ue', + 'Ö' => 'Oe', + 'À' => 'A', + 'Á' => 'A', + 'Â' => 'A', + 'Ã' => 'A', + 'Å' => 'A', + 'Ǻ' => 'A', + 'Ā' => 'A', + 'Ă' => 'A', + 'Ą' => 'A', + 'Ǎ' => 'A', + 'à' => 'a', + 'á' => 'a', + 'â' => 'a', + 'ã' => 'a', + 'å' => 'a', + 'ǻ' => 'a', + 'ā' => 'a', + 'ă' => 'a', + 'ą' => 'a', + 'ǎ' => 'a', + 'ª' => 'a', + 'Ç' => 'C', + 'Ć' => 'C', + 'Ĉ' => 'C', + 'Ċ' => 'C', + 'Č' => 'C', + 'ç' => 'c', + 'ć' => 'c', + 'ĉ' => 'c', + 'ċ' => 'c', + 'č' => 'c', + 'Ð' => 'D', + 'Ď' => 'D', + 'Đ' => 'D', + 'ð' => 'd', + 'ď' => 'd', + 'đ' => 'd', + 'È' => 'E', + 'É' => 'E', + 'Ê' => 'E', + 'Ë' => 'E', + 'Ē' => 'E', + 'Ĕ' => 'E', + 'Ė' => 'E', + 'Ę' => 'E', + 'Ě' => 'E', + 'è' => 'e', + 'é' => 'e', + 'ê' => 'e', + 'ë' => 'e', + 'ē' => 'e', + 'ĕ' => 'e', + 'ė' => 'e', + 'ę' => 'e', + 'ě' => 'e', + 'Ĝ' => 'G', + 'Ğ' => 'G', + 'Ġ' => 'G', + 'Ģ' => 'G', + 'Ґ' => 'G', + 'ĝ' => 'g', + 'ğ' => 'g', + 'ġ' => 'g', + 'ģ' => 'g', + 'ґ' => 'g', + 'Ĥ' => 'H', + 'Ħ' => 'H', + 'ĥ' => 'h', + 'ħ' => 'h', + 'І' => 'I', + 'Ì' => 'I', + 'Í' => 'I', + 'Î' => 'I', + 'Ї' => 'Yi', + 'Ï' => 'I', + 'Ĩ' => 'I', + 'Ī' => 'I', + 'Ĭ' => 'I', + 'Ǐ' => 'I', + 'Į' => 'I', + 'İ' => 'I', + 'і' => 'i', + 'ì' => 'i', + 'í' => 'i', + 'î' => 'i', + 'ï' => 'i', + 'ї' => 'yi', + 'ĩ' => 'i', + 'ī' => 'i', + 'ĭ' => 'i', + 'ǐ' => 'i', + 'į' => 'i', + 'ı' => 'i', + 'Ĵ' => 'J', + 'ĵ' => 'j', + 'Ķ' => 'K', + 'ķ' => 'k', + 'Ĺ' => 'L', + 'Ļ' => 'L', + 'Ľ' => 'L', + 'Ŀ' => 'L', + 'Ł' => 'L', + 'ĺ' => 'l', + 'ļ' => 'l', + 'ľ' => 'l', + 'ŀ' => 'l', + 'ł' => 'l', + 'Ñ' => 'N', + 'Ń' => 'N', + 'Ņ' => 'N', + 'Ň' => 'N', + 'ñ' => 'n', + 'ń' => 'n', + 'ņ' => 'n', + 'ň' => 'n', + 'ʼn' => 'n', + 'Ò' => 'O', + 'Ó' => 'O', + 'Ô' => 'O', + 'Õ' => 'O', + 'Ō' => 'O', + 'Ŏ' => 'O', + 'Ǒ' => 'O', + 'Ő' => 'O', + 'Ơ' => 'O', + 'Ø' => 'O', + 'Ǿ' => 'O', + 'ò' => 'o', + 'ó' => 'o', + 'ô' => 'o', + 'õ' => 'o', + 'ō' => 'o', + 'ŏ' => 'o', + 'ǒ' => 'o', + 'ő' => 'o', + 'ơ' => 'o', + 'ø' => 'o', + 'ǿ' => 'o', + 'º' => 'o', + 'Ŕ' => 'R', + 'Ŗ' => 'R', + 'Ř' => 'R', + 'ŕ' => 'r', + 'ŗ' => 'r', + 'ř' => 'r', + 'Ś' => 'S', + 'Ŝ' => 'S', + 'Ş' => 'S', + 'Ș' => 'S', + 'Š' => 'S', + 'ẞ' => 'SS', + 'ś' => 's', + 'ŝ' => 's', + 'ş' => 's', + 'ș' => 's', + 'š' => 's', + 'ſ' => 's', + 'Ţ' => 'T', + 'Ț' => 'T', + 'Ť' => 'T', + 'Ŧ' => 'T', + 'ţ' => 't', + 'ț' => 't', + 'ť' => 't', + 'ŧ' => 't', + 'Ù' => 'U', + 'Ú' => 'U', + 'Û' => 'U', + 'Ũ' => 'U', + 'Ū' => 'U', + 'Ŭ' => 'U', + 'Ů' => 'U', + 'Ű' => 'U', + 'Ų' => 'U', + 'Ư' => 'U', + 'Ǔ' => 'U', + 'Ǖ' => 'U', + 'Ǘ' => 'U', + 'Ǚ' => 'U', + 'Ǜ' => 'U', + 'ù' => 'u', + 'ú' => 'u', + 'û' => 'u', + 'ũ' => 'u', + 'ū' => 'u', + 'ŭ' => 'u', + 'ů' => 'u', + 'ű' => 'u', + 'ų' => 'u', + 'ư' => 'u', + 'ǔ' => 'u', + 'ǖ' => 'u', + 'ǘ' => 'u', + 'ǚ' => 'u', + 'ǜ' => 'u', + 'Ý' => 'Y', + 'Ÿ' => 'Y', + 'Ŷ' => 'Y', + 'ý' => 'y', + 'ÿ' => 'y', + 'ŷ' => 'y', + 'Ŵ' => 'W', + 'ŵ' => 'w', + 'Ź' => 'Z', + 'Ż' => 'Z', + 'Ž' => 'Z', + 'ź' => 'z', + 'ż' => 'z', + 'ž' => 'z', + 'Æ' => 'AE', + 'Ǽ' => 'AE', + 'ß' => 'ss', + 'IJ' => 'IJ', + 'ij' => 'ij', + 'Œ' => 'OE', + 'ƒ' => 'f', + 'Þ' => 'TH', + 'þ' => 'th', + 'Є' => 'Ye', + 'є' => 'ye', + ]; + + /** + * Method cache array. + * + * @var array + */ + protected static $_cache = []; + + /** + * The initial state of Inflector so reset() works. + * + * @var array + */ + protected static $_initialState = []; + + /** + * Cache inflected values, and return if already available + * + * @param string $type Inflection type + * @param string $key Original value + * @param string|bool $value Inflected value + * @return string|false Inflected value on cache hit or false on cache miss. + */ + protected static function _cache($type, $key, $value = false) + { + $key = '_' . $key; + $type = '_' . $type; + if ($value !== false) { + static::$_cache[$type][$key] = $value; + + return $value; + } + if (!isset(static::$_cache[$type][$key])) { + return false; + } + + return static::$_cache[$type][$key]; + } + + /** + * Clears Inflectors inflected value caches. And resets the inflection + * rules to the initial values. + * + * @return void + */ + public static function reset() + { + if (empty(static::$_initialState)) { + static::$_initialState = get_class_vars(__CLASS__); + + return; + } + foreach (static::$_initialState as $key => $val) { + if ($key !== '_initialState') { + static::${$key} = $val; + } + } + } + + /** + * Adds custom inflection $rules, of either 'plural', 'singular', + * 'uninflected', 'irregular' or 'transliteration' $type. + * + * ### Usage: + * + * ``` + * Inflector::rules('plural', ['/^(inflect)or$/i' => '\1ables']); + * Inflector::rules('irregular', ['red' => 'redlings']); + * Inflector::rules('uninflected', ['dontinflectme']); + * Inflector::rules('transliteration', ['/å/' => 'aa']); + * ``` + * + * @param string $type The type of inflection, either 'plural', 'singular', + * 'uninflected' or 'transliteration'. + * @param array $rules Array of rules to be added. + * @param bool $reset If true, will unset default inflections for all + * new rules that are being defined in $rules. + * @return void + */ + public static function rules($type, $rules, $reset = false) + { + $var = '_' . $type; + + if ($reset) { + static::${$var} = $rules; + } elseif ($type === 'uninflected') { + static::$_uninflected = array_merge( + $rules, + static::$_uninflected + ); + } else { + static::${$var} = $rules + static::${$var}; + } + + static::$_cache = []; + } + + /** + * Return $word in plural form. + * + * @param string $word Word in singular + * @return string Word in plural + * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-plural-singular-forms + */ + public static function pluralize($word) + { + if (isset(static::$_cache['pluralize'][$word])) { + return static::$_cache['pluralize'][$word]; + } + + if (!isset(static::$_cache['irregular']['pluralize'])) { + static::$_cache['irregular']['pluralize'] = '(?:' . implode('|', array_keys(static::$_irregular)) . ')'; + } + + if (preg_match('/(.*?(?:\\b|_))(' . static::$_cache['irregular']['pluralize'] . ')$/i', $word, $regs)) { + static::$_cache['pluralize'][$word] = $regs[1] . substr($regs[2], 0, 1) . + substr(static::$_irregular[strtolower($regs[2])], 1); + + return static::$_cache['pluralize'][$word]; + } + + if (!isset(static::$_cache['uninflected'])) { + static::$_cache['uninflected'] = '(?:' . implode('|', static::$_uninflected) . ')'; + } + + if (preg_match('/^(' . static::$_cache['uninflected'] . ')$/i', $word, $regs)) { + static::$_cache['pluralize'][$word] = $word; + + return $word; + } + + foreach (static::$_plural as $rule => $replacement) { + if (preg_match($rule, $word)) { + static::$_cache['pluralize'][$word] = preg_replace($rule, $replacement, $word); + + return static::$_cache['pluralize'][$word]; + } + } + } + + /** + * Return $word in singular form. + * + * @param string $word Word in plural + * @return string Word in singular + * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-plural-singular-forms + */ + public static function singularize($word) + { + if (isset(static::$_cache['singularize'][$word])) { + return static::$_cache['singularize'][$word]; + } + + if (!isset(static::$_cache['irregular']['singular'])) { + static::$_cache['irregular']['singular'] = '(?:' . implode('|', static::$_irregular) . ')'; + } + + if (preg_match('/(.*?(?:\\b|_))(' . static::$_cache['irregular']['singular'] . ')$/i', $word, $regs)) { + static::$_cache['singularize'][$word] = $regs[1] . substr($regs[2], 0, 1) . + substr(array_search(strtolower($regs[2]), static::$_irregular), 1); + + return static::$_cache['singularize'][$word]; + } + + if (!isset(static::$_cache['uninflected'])) { + static::$_cache['uninflected'] = '(?:' . implode('|', static::$_uninflected) . ')'; + } + + if (preg_match('/^(' . static::$_cache['uninflected'] . ')$/i', $word, $regs)) { + static::$_cache['pluralize'][$word] = $word; + + return $word; + } + + foreach (static::$_singular as $rule => $replacement) { + if (preg_match($rule, $word)) { + static::$_cache['singularize'][$word] = preg_replace($rule, $replacement, $word); + + return static::$_cache['singularize'][$word]; + } + } + static::$_cache['singularize'][$word] = $word; + + return $word; + } + + /** + * Returns the input lower_case_delimited_string as a CamelCasedString. + * + * @param string $string String to camelize + * @param string $delimiter the delimiter in the input string + * @return string CamelizedStringLikeThis. + * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-camelcase-and-under-scored-forms + */ + public static function camelize($string, $delimiter = '_') + { + $cacheKey = __FUNCTION__ . $delimiter; + + $result = static::_cache($cacheKey, $string); + + if ($result === false) { + $result = str_replace(' ', '', static::humanize($string, $delimiter)); + static::_cache($cacheKey, $string, $result); + } + + return $result; + } + + /** + * Returns the input CamelCasedString as an underscored_string. + * + * Also replaces dashes with underscores + * + * @param string $string CamelCasedString to be "underscorized" + * @return string underscore_version of the input string + * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-camelcase-and-under-scored-forms + */ + public static function underscore($string) + { + return static::delimit(str_replace('-', '_', $string), '_'); + } + + /** + * Returns the input CamelCasedString as an dashed-string. + * + * Also replaces underscores with dashes + * + * @param string $string The string to dasherize. + * @return string Dashed version of the input string + */ + public static function dasherize($string) + { + return static::delimit(str_replace('_', '-', $string), '-'); + } + + /** + * Returns the input lower_case_delimited_string as 'A Human Readable String'. + * (Underscores are replaced by spaces and capitalized following words.) + * + * @param string $string String to be humanized + * @param string $delimiter the character to replace with a space + * @return string Human-readable string + * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-human-readable-forms + */ + public static function humanize($string, $delimiter = '_') + { + $cacheKey = __FUNCTION__ . $delimiter; + + $result = static::_cache($cacheKey, $string); + + if ($result === false) { + $result = explode(' ', str_replace($delimiter, ' ', $string)); + foreach ($result as &$word) { + $word = mb_strtoupper(mb_substr($word, 0, 1)) . mb_substr($word, 1); + } + $result = implode(' ', $result); + static::_cache($cacheKey, $string, $result); + } + + return $result; + } + + /** + * Expects a CamelCasedInputString, and produces a lower_case_delimited_string + * + * @param string $string String to delimit + * @param string $delimiter the character to use as a delimiter + * @return string delimited string + */ + public static function delimit($string, $delimiter = '_') + { + $cacheKey = __FUNCTION__ . $delimiter; + + $result = static::_cache($cacheKey, $string); + + if ($result === false) { + $result = mb_strtolower(preg_replace('/(?<=\\w)([A-Z])/', $delimiter . '\\1', $string)); + static::_cache($cacheKey, $string, $result); + } + + return $result; + } + + /** + * Returns corresponding table name for given model $className. ("people" for the model class "Person"). + * + * @param string $className Name of class to get database table name for + * @return string Name of the database table for given class + * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-table-and-class-name-forms + */ + public static function tableize($className) + { + $result = static::_cache(__FUNCTION__, $className); + + if ($result === false) { + $result = static::pluralize(static::underscore($className)); + static::_cache(__FUNCTION__, $className, $result); + } + + return $result; + } + + /** + * Returns Cake model class name ("Person" for the database table "people".) for given database table. + * + * @param string $tableName Name of database table to get class name for + * @return string Class name + * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-table-and-class-name-forms + */ + public static function classify($tableName) + { + $result = static::_cache(__FUNCTION__, $tableName); + + if ($result === false) { + $result = static::camelize(static::singularize($tableName)); + static::_cache(__FUNCTION__, $tableName, $result); + } + + return $result; + } + + /** + * Returns camelBacked version of an underscored string. + * + * @param string $string String to convert. + * @return string in variable form + * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-variable-names + */ + public static function variable($string) + { + $result = static::_cache(__FUNCTION__, $string); + + if ($result === false) { + $camelized = static::camelize(static::underscore($string)); + $replace = strtolower(substr($camelized, 0, 1)); + $result = $replace . substr($camelized, 1); + static::_cache(__FUNCTION__, $string, $result); + } + + return $result; + } + + /** + * Returns a string with all spaces converted to dashes (by default), accented + * characters converted to non-accented characters, and non word characters removed. + * + * @deprecated 3.2.7 Use Text::slug() instead. + * @param string $string the string you want to slug + * @param string $replacement will replace keys in map + * @return string + * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-url-safe-strings + */ + public static function slug($string, $replacement = '-') + { + deprecationWarning( + 'Inflector::slug() is deprecated. ' . + 'Use Text::slug() instead.' + ); + $quotedReplacement = preg_quote($replacement, '/'); + + $map = [ + '/[^\s\p{Zs}\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}]/mu' => ' ', + '/[\s\p{Zs}]+/mu' => $replacement, + sprintf('/^[%s]+|[%s]+$/', $quotedReplacement, $quotedReplacement) => '', + ]; + + $string = str_replace( + array_keys(static::$_transliteration), + static::$_transliteration, + $string + ); + + return preg_replace(array_keys($map), array_values($map), $string); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Utility/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Utility/LICENSE.txt new file mode 100644 index 000000000..0c4b7932c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Utility/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2016, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/vendor/cakephp/cakephp/src/Utility/MergeVariablesTrait.php b/app/vendor/cakephp/cakephp/src/Utility/MergeVariablesTrait.php new file mode 100644 index 000000000..6f676518d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Utility/MergeVariablesTrait.php @@ -0,0 +1,116 @@ +{$property}; + if ($thisValue === null || $thisValue === false) { + continue; + } + $this->_mergeProperty($property, $parents, $options); + } + } + + /** + * Merge a single property with the values declared in all parent classes. + * + * @param string $property The name of the property being merged. + * @param array $parentClasses An array of classes you want to merge with. + * @param array $options Options for merging the property, see _mergeVars() + * @return void + */ + protected function _mergeProperty($property, $parentClasses, $options) + { + $thisValue = $this->{$property}; + $isAssoc = false; + if (isset($options['associative']) && + in_array($property, (array)$options['associative']) + ) { + $isAssoc = true; + } + + if ($isAssoc) { + $thisValue = Hash::normalize($thisValue); + } + foreach ($parentClasses as $class) { + $parentProperties = get_class_vars($class); + if (empty($parentProperties[$property])) { + continue; + } + $parentProperty = $parentProperties[$property]; + if (!is_array($parentProperty)) { + continue; + } + $thisValue = $this->_mergePropertyData($thisValue, $parentProperty, $isAssoc); + } + $this->{$property} = $thisValue; + } + + /** + * Merge each of the keys in a property together. + * + * @param array $current The current merged value. + * @param array $parent The parent class' value. + * @param bool $isAssoc Whether or not the merging should be done in associative mode. + * @return mixed The updated value. + */ + protected function _mergePropertyData($current, $parent, $isAssoc) + { + if (!$isAssoc) { + return array_merge($parent, $current); + } + $parent = Hash::normalize($parent); + foreach ($parent as $key => $value) { + if (!isset($current[$key])) { + $current[$key] = $value; + } + } + + return $current; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Utility/README.md b/app/vendor/cakephp/cakephp/src/Utility/README.md new file mode 100644 index 000000000..3183e002c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Utility/README.md @@ -0,0 +1,91 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/utility.svg?style=flat-square)](https://packagist.org/packages/cakephp/utility) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# CakePHP Utility Classes + +This library provides a range of utility classes that are used throughout the CakePHP framework + +## What's in the toolbox? + +### Hash + +A ``Hash`` (as in PHP arrays) class, capable of extracting data using an intuitive DSL: + +```php +$things = [ + ['name' => 'Mark', 'age' => 15], + ['name' => 'Susan', 'age' => 30], + ['name' => 'Lucy', 'age' => 25] +]; + +$bigPeople = Hash::extract($things, '{n}[age>21].name'); + +// $bigPeople will contain ['Susan', 'Lucy'] +``` + +Check the [official Hash class documentation](https://book.cakephp.org/3.0/en/core-libraries/hash.html) + +### Inflector + +The Inflector class takes a string and can manipulate it to handle word variations +such as pluralizations or camelizing. + +```php +echo Inflector::pluralize('Apple'); // echoes Apples + +echo Inflector::singularize('People'); // echoes Person +``` + +Check the [official Inflector class documentation](https://book.cakephp.org/3.0/en/core-libraries/inflector.html) + +### Text + +The Text class includes convenience methods for creating and manipulating strings. + +```php +Text::insert( + 'My name is :name and I am :age years old.', + ['name' => 'Bob', 'age' => '65'] +); +// Returns: "My name is Bob and I am 65 years old." + +$text = 'This is the song that never ends.'; +$result = Text::wrap($text, 22); + +// Returns +This is the song +that never ends. +``` + +Check the [official Text class documentation](https://book.cakephp.org/3.0/en/core-libraries/text.html) + +### Security + +The security library handles basic security measures such as providing methods for hashing and encrypting data. + +```php +$key = 'wt1U5MACWJFTXGenFoZoiLwQGrLgdbHA'; +$result = Security::encrypt($value, $key); + +Security::decrypt($result, $key); +``` + +Check the [official Security class documentation](https://book.cakephp.org/3.0/en/core-libraries/security.html) + +### Xml + +The Xml class allows you to easily transform arrays into SimpleXMLElement or DOMDocument objects +and back into arrays again + +```php +$data = [ + 'post' => [ + 'id' => 1, + 'title' => 'Best post', + 'body' => ' ... ' + ] +]; +$xml = Xml::build($data); +``` + +Check the [official Xml class documentation](https://book.cakephp.org/3.0/en/core-libraries/xml.html) diff --git a/app/vendor/cakephp/cakephp/src/Utility/Security.php b/app/vendor/cakephp/cakephp/src/Utility/Security.php new file mode 100644 index 000000000..e80098103 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Utility/Security.php @@ -0,0 +1,391 @@ +rijndael($text, $key, $operation); + } + + /** + * Encrypt a value using AES-256. + * + * *Caveat* You cannot properly encrypt/decrypt data with trailing null bytes. + * Any trailing null bytes will be removed on decryption due to how PHP pads messages + * with nulls prior to encryption. + * + * @param string $plain The value to encrypt. + * @param string $key The 256 bit/32 byte key to use as a cipher key. + * @param string|null $hmacSalt The salt to use for the HMAC process. Leave null to use Security.salt. + * @return string Encrypted data. + * @throws \InvalidArgumentException On invalid data or key. + */ + public static function encrypt($plain, $key, $hmacSalt = null) + { + self::_checkKey($key, 'encrypt()'); + + if ($hmacSalt === null) { + $hmacSalt = static::$_salt; + } + // Generate the encryption and hmac key. + $key = mb_substr(hash('sha256', $key . $hmacSalt), 0, 32, '8bit'); + + $crypto = static::engine(); + $ciphertext = $crypto->encrypt($plain, $key); + $hmac = hash_hmac('sha256', $ciphertext, $key); + + return $hmac . $ciphertext; + } + + /** + * Check the encryption key for proper length. + * + * @param string $key Key to check. + * @param string $method The method the key is being checked for. + * @return void + * @throws \InvalidArgumentException When key length is not 256 bit/32 bytes + */ + protected static function _checkKey($key, $method) + { + if (mb_strlen($key, '8bit') < 32) { + throw new InvalidArgumentException( + sprintf('Invalid key for %s, key must be at least 256 bits (32 bytes) long.', $method) + ); + } + } + + /** + * Decrypt a value using AES-256. + * + * @param string $cipher The ciphertext to decrypt. + * @param string $key The 256 bit/32 byte key to use as a cipher key. + * @param string|null $hmacSalt The salt to use for the HMAC process. Leave null to use Security.salt. + * @return string|bool Decrypted data. Any trailing null bytes will be removed. + * @throws \InvalidArgumentException On invalid data or key. + */ + public static function decrypt($cipher, $key, $hmacSalt = null) + { + self::_checkKey($key, 'decrypt()'); + if (empty($cipher)) { + throw new InvalidArgumentException('The data to decrypt cannot be empty.'); + } + if ($hmacSalt === null) { + $hmacSalt = static::$_salt; + } + + // Generate the encryption and hmac key. + $key = mb_substr(hash('sha256', $key . $hmacSalt), 0, 32, '8bit'); + + // Split out hmac for comparison + $macSize = 64; + $hmac = mb_substr($cipher, 0, $macSize, '8bit'); + $cipher = mb_substr($cipher, $macSize, null, '8bit'); + + $compareHmac = hash_hmac('sha256', $cipher, $key); + if (!static::constantEquals($hmac, $compareHmac)) { + return false; + } + + $crypto = static::engine(); + + return $crypto->decrypt($cipher, $key); + } + + /** + * A timing attack resistant comparison that prefers native PHP implementations. + * + * @param string $original The original value. + * @param string $compare The comparison value. + * @return bool + * @see https://github.com/resonantcore/php-future/ + * @since 3.6.2 + */ + public static function constantEquals($original, $compare) + { + if (!is_string($original) || !is_string($compare)) { + return false; + } + if (function_exists('hash_equals')) { + return hash_equals($original, $compare); + } + $originalLength = mb_strlen($original, '8bit'); + $compareLength = mb_strlen($compare, '8bit'); + if ($originalLength !== $compareLength) { + return false; + } + $result = 0; + for ($i = 0; $i < $originalLength; $i++) { + $result |= (ord($original[$i]) ^ ord($compare[$i])); + } + + return $result === 0; + } + + /** + * Gets the HMAC salt to be used for encryption/decryption + * routines. + * + * @return string The currently configured salt + */ + public static function getSalt() + { + return static::$_salt; + } + + /** + * Sets the HMAC salt to be used for encryption/decryption + * routines. + * + * @param string $salt The salt to use for encryption routines. + * @return void + */ + public static function setSalt($salt) + { + static::$_salt = (string)$salt; + } + + /** + * Gets or sets the HMAC salt to be used for encryption/decryption + * routines. + * + * @deprecated 3.5.0 Use getSalt()/setSalt() instead. + * @param string|null $salt The salt to use for encryption routines. If null returns current salt. + * @return string The currently configured salt + */ + public static function salt($salt = null) + { + deprecationWarning( + 'Security::salt() is deprecated. ' . + 'Use Security::getSalt()/setSalt() instead.' + ); + if ($salt === null) { + return static::$_salt; + } + + return static::$_salt = (string)$salt; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Utility/String.php b/app/vendor/cakephp/cakephp/src/Utility/String.php new file mode 100644 index 000000000..e30ba9cf4 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Utility/String.php @@ -0,0 +1,6 @@ + 'Bob', 'age' => '65']); + * ``` + * Returns: Bob is 65 years old. + * + * Available $options are: + * + * - before: The character or string in front of the name of the variable placeholder (Defaults to `:`) + * - after: The character or string after the name of the variable placeholder (Defaults to null) + * - escape: The character or string used to escape the before character / string (Defaults to `\`) + * - format: A regex to use for matching variable placeholders. Default is: `/(? val array where each key stands for a placeholder variable name + * to be replaced with val + * @param array $options An array of options, see description above + * @return string + */ + public static function insert($str, $data, array $options = []) + { + $defaults = [ + 'before' => ':', 'after' => null, 'escape' => '\\', 'format' => null, 'clean' => false + ]; + $options += $defaults; + $format = $options['format']; + $data = (array)$data; + if (empty($data)) { + return $options['clean'] ? static::cleanInsert($str, $options) : $str; + } + + if (!isset($format)) { + $format = sprintf( + '/(? $hashVal) { + $key = sprintf($format, preg_quote($key, '/')); + $str = preg_replace($key, $hashVal, $str); + } + $dataReplacements = array_combine($hashKeys, array_values($data)); + foreach ($dataReplacements as $tmpHash => $tmpValue) { + $tmpValue = is_array($tmpValue) ? '' : $tmpValue; + $str = str_replace($tmpHash, $tmpValue, $str); + } + + if (!isset($options['format']) && isset($options['before'])) { + $str = str_replace($options['escape'] . $options['before'], $options['before'], $str); + } + + return $options['clean'] ? static::cleanInsert($str, $options) : $str; + } + + /** + * Cleans up a Text::insert() formatted string with given $options depending on the 'clean' key in + * $options. The default method used is text but html is also available. The goal of this function + * is to replace all whitespace and unneeded markup around placeholders that did not get replaced + * by Text::insert(). + * + * @param string $str String to clean. + * @param array $options Options list. + * @return string + * @see \Cake\Utility\Text::insert() + */ + public static function cleanInsert($str, array $options) + { + $clean = $options['clean']; + if (!$clean) { + return $str; + } + if ($clean === true) { + $clean = ['method' => 'text']; + } + if (!is_array($clean)) { + $clean = ['method' => $options['clean']]; + } + switch ($clean['method']) { + case 'html': + $clean += [ + 'word' => '[\w,.]+', + 'andText' => true, + 'replacement' => '', + ]; + $kleenex = sprintf( + '/[\s]*[a-z]+=(")(%s%s%s[\s]*)+\\1/i', + preg_quote($options['before'], '/'), + $clean['word'], + preg_quote($options['after'], '/') + ); + $str = preg_replace($kleenex, $clean['replacement'], $str); + if ($clean['andText']) { + $options['clean'] = ['method' => 'text']; + $str = static::cleanInsert($str, $options); + } + break; + case 'text': + $clean += [ + 'word' => '[\w,.]+', + 'gap' => '[\s]*(?:(?:and|or)[\s]*)?', + 'replacement' => '', + ]; + + $kleenex = sprintf( + '/(%s%s%s%s|%s%s%s%s)/', + preg_quote($options['before'], '/'), + $clean['word'], + preg_quote($options['after'], '/'), + $clean['gap'], + $clean['gap'], + preg_quote($options['before'], '/'), + $clean['word'], + preg_quote($options['after'], '/') + ); + $str = preg_replace($kleenex, $clean['replacement'], $str); + break; + } + + return $str; + } + + /** + * Wraps text to a specific width, can optionally wrap at word breaks. + * + * ### Options + * + * - `width` The width to wrap to. Defaults to 72. + * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true. + * - `indent` String to indent with. Defaults to null. + * - `indentAt` 0 based index to start indenting at. Defaults to 0. + * + * @param string $text The text to format. + * @param array|int $options Array of options to use, or an integer to wrap the text to. + * @return string Formatted text. + */ + public static function wrap($text, $options = []) + { + if (is_numeric($options)) { + $options = ['width' => $options]; + } + $options += ['width' => 72, 'wordWrap' => true, 'indent' => null, 'indentAt' => 0]; + if ($options['wordWrap']) { + $wrapped = self::wordWrap($text, $options['width'], "\n"); + } else { + $wrapped = trim(chunk_split($text, $options['width'] - 1, "\n")); + } + if (!empty($options['indent'])) { + $chunks = explode("\n", $wrapped); + for ($i = $options['indentAt'], $len = count($chunks); $i < $len; $i++) { + $chunks[$i] = $options['indent'] . $chunks[$i]; + } + $wrapped = implode("\n", $chunks); + } + + return $wrapped; + } + + /** + * Wraps a complete block of text to a specific width, can optionally wrap + * at word breaks. + * + * ### Options + * + * - `width` The width to wrap to. Defaults to 72. + * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true. + * - `indent` String to indent with. Defaults to null. + * - `indentAt` 0 based index to start indenting at. Defaults to 0. + * + * @param string $text The text to format. + * @param array|int $options Array of options to use, or an integer to wrap the text to. + * @return string Formatted text. + */ + public static function wrapBlock($text, $options = []) + { + if (is_numeric($options)) { + $options = ['width' => $options]; + } + $options += ['width' => 72, 'wordWrap' => true, 'indent' => null, 'indentAt' => 0]; + + if (!empty($options['indentAt']) && $options['indentAt'] === 0) { + $indentLength = !empty($options['indent']) ? strlen($options['indent']) : 0; + $options['width'] -= $indentLength; + + return self::wrap($text, $options); + } + + $wrapped = self::wrap($text, $options); + + if (!empty($options['indent'])) { + $indentationLength = mb_strlen($options['indent']); + $chunks = explode("\n", $wrapped); + $count = count($chunks); + if ($count < 2) { + return $wrapped; + } + $toRewrap = ''; + for ($i = $options['indentAt']; $i < $count; $i++) { + $toRewrap .= mb_substr($chunks[$i], $indentationLength) . ' '; + unset($chunks[$i]); + } + $options['width'] -= $indentationLength; + $options['indentAt'] = 0; + $rewrapped = self::wrap($toRewrap, $options); + $newChunks = explode("\n", $rewrapped); + + $chunks = array_merge($chunks, $newChunks); + $wrapped = implode("\n", $chunks); + } + + return $wrapped; + } + + /** + * Unicode and newline aware version of wordwrap. + * + * @param string $text The text to format. + * @param int $width The width to wrap to. Defaults to 72. + * @param string $break The line is broken using the optional break parameter. Defaults to '\n'. + * @param bool $cut If the cut is set to true, the string is always wrapped at the specified width. + * @return string Formatted text. + */ + public static function wordWrap($text, $width = 72, $break = "\n", $cut = false) + { + $paragraphs = explode($break, $text); + foreach ($paragraphs as &$paragraph) { + $paragraph = static::_wordWrap($paragraph, $width, $break, $cut); + } + + return implode($break, $paragraphs); + } + + /** + * Unicode aware version of wordwrap as helper method. + * + * @param string $text The text to format. + * @param int $width The width to wrap to. Defaults to 72. + * @param string $break The line is broken using the optional break parameter. Defaults to '\n'. + * @param bool $cut If the cut is set to true, the string is always wrapped at the specified width. + * @return string Formatted text. + */ + protected static function _wordWrap($text, $width = 72, $break = "\n", $cut = false) + { + if ($cut) { + $parts = []; + while (mb_strlen($text) > 0) { + $part = mb_substr($text, 0, $width); + $parts[] = trim($part); + $text = trim(mb_substr($text, mb_strlen($part))); + } + + return implode($break, $parts); + } + + $parts = []; + while (mb_strlen($text) > 0) { + if ($width >= mb_strlen($text)) { + $parts[] = trim($text); + break; + } + + $part = mb_substr($text, 0, $width); + $nextChar = mb_substr($text, $width, 1); + if ($nextChar !== ' ') { + $breakAt = mb_strrpos($part, ' '); + if ($breakAt === false) { + $breakAt = mb_strpos($text, ' ', $width); + } + if ($breakAt === false) { + $parts[] = trim($text); + break; + } + $part = mb_substr($text, 0, $breakAt); + } + + $part = trim($part); + $parts[] = $part; + $text = trim(mb_substr($text, mb_strlen($part))); + } + + return implode($break, $parts); + } + + /** + * Highlights a given phrase in a text. You can specify any expression in highlighter that + * may include the \1 expression to include the $phrase found. + * + * ### Options: + * + * - `format` The piece of HTML with that the phrase will be highlighted + * - `html` If true, will ignore any HTML tags, ensuring that only the correct text is highlighted + * - `regex` A custom regex rule that is used to match words, default is '|$tag|iu' + * - `limit` A limit, optional, defaults to -1 (none) + * + * @param string $text Text to search the phrase in. + * @param string|array $phrase The phrase or phrases that will be searched. + * @param array $options An array of HTML attributes and options. + * @return string The highlighted text + * @link https://book.cakephp.org/3.0/en/core-libraries/text.html#highlighting-substrings + */ + public static function highlight($text, $phrase, array $options = []) + { + if (empty($phrase)) { + return $text; + } + + $defaults = [ + 'format' => '\1', + 'html' => false, + 'regex' => '|%s|iu', + 'limit' => -1, + ]; + $options += $defaults; + $html = $format = $ellipsis = $exact = $limit = null; + extract($options); + + if (is_array($phrase)) { + $replace = []; + $with = []; + + foreach ($phrase as $key => $segment) { + $segment = '(' . preg_quote($segment, '|') . ')'; + if ($html) { + $segment = "(?![^<]+>)$segment(?![^<]+>)"; + } + + $with[] = is_array($format) ? $format[$key] : $format; + $replace[] = sprintf($options['regex'], $segment); + } + + return preg_replace($replace, $with, $text, $limit); + } + + $phrase = '(' . preg_quote($phrase, '|') . ')'; + if ($html) { + $phrase = "(?![^<]+>)$phrase(?![^<]+>)"; + } + + return preg_replace(sprintf($options['regex'], $phrase), $format, $text, $limit); + } + + /** + * Strips given text of all links (]*)?(>|$)#i', '', $text, -1, $count); + } while ($count); + + return $text; + } + + /** + * Truncates text starting from the end. + * + * Cuts a string to the length of $length and replaces the first characters + * with the ellipsis if the text is longer than length. + * + * ### Options: + * + * - `ellipsis` Will be used as Beginning and prepended to the trimmed string + * - `exact` If false, $text will not be cut mid-word + * + * @param string $text String to truncate. + * @param int $length Length of returned string, including ellipsis. + * @param array $options An array of options. + * @return string Trimmed string. + */ + public static function tail($text, $length = 100, array $options = []) + { + $default = [ + 'ellipsis' => '...', 'exact' => true + ]; + $options += $default; + $exact = $ellipsis = null; + extract($options); + + if (mb_strlen($text) <= $length) { + return $text; + } + + $truncate = mb_substr($text, mb_strlen($text) - $length + mb_strlen($ellipsis)); + if (!$exact) { + $spacepos = mb_strpos($truncate, ' '); + $truncate = $spacepos === false ? '' : trim(mb_substr($truncate, $spacepos)); + } + + return $ellipsis . $truncate; + } + + /** + * Truncates text. + * + * Cuts a string to the length of $length and replaces the last characters + * with the ellipsis if the text is longer than length. + * + * ### Options: + * + * - `ellipsis` Will be used as ending and appended to the trimmed string + * - `exact` If false, $text will not be cut mid-word + * - `html` If true, HTML tags would be handled correctly + * - `trimWidth` If true, $text will be truncated with the width + * + * @param string $text String to truncate. + * @param int $length Length of returned string, including ellipsis. + * @param array $options An array of HTML attributes and options. + * @return string Trimmed string. + * @link https://book.cakephp.org/3.0/en/core-libraries/text.html#truncating-text + */ + public static function truncate($text, $length = 100, array $options = []) + { + $default = [ + 'ellipsis' => '...', 'exact' => true, 'html' => false, 'trimWidth' => false, + ]; + if (!empty($options['html']) && strtolower(mb_internal_encoding()) === 'utf-8') { + $default['ellipsis'] = "\xe2\x80\xa6"; + } + $options += $default; + + $prefix = ''; + $suffix = $options['ellipsis']; + + if ($options['html']) { + $ellipsisLength = self::_strlen(strip_tags($options['ellipsis']), $options); + + $truncateLength = 0; + $totalLength = 0; + $openTags = []; + $truncate = ''; + + preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER); + foreach ($tags as $tag) { + $contentLength = 0; + if (!in_array($tag[2], static::$_defaultHtmlNoCount, true)) { + $contentLength = self::_strlen($tag[3], $options); + } + + if ($truncate === '') { + if (!preg_match('/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/i', $tag[2])) { + if (preg_match('/<[\w]+[^>]*>/', $tag[0])) { + array_unshift($openTags, $tag[2]); + } elseif (preg_match('/<\/([\w]+)[^>]*>/', $tag[0], $closeTag)) { + $pos = array_search($closeTag[1], $openTags); + if ($pos !== false) { + array_splice($openTags, $pos, 1); + } + } + } + + $prefix .= $tag[1]; + + if ($totalLength + $contentLength + $ellipsisLength > $length) { + $truncate = $tag[3]; + $truncateLength = $length - $totalLength; + } else { + $prefix .= $tag[3]; + } + } + + $totalLength += $contentLength; + if ($totalLength > $length) { + break; + } + } + + if ($totalLength <= $length) { + return $text; + } + + $text = $truncate; + $length = $truncateLength; + + foreach ($openTags as $tag) { + $suffix .= ''; + } + } else { + if (self::_strlen($text, $options) <= $length) { + return $text; + } + $ellipsisLength = self::_strlen($options['ellipsis'], $options); + } + + $result = self::_substr($text, 0, $length - $ellipsisLength, $options); + + if (!$options['exact']) { + if (self::_substr($text, $length - $ellipsisLength, 1, $options) !== ' ') { + $result = self::_removeLastWord($result); + } + + // If result is empty, then we don't need to count ellipsis in the cut. + if (!strlen($result)) { + $result = self::_substr($text, 0, $length, $options); + } + } + + return $prefix . $result . $suffix; + } + + /** + * Truncate text with specified width. + * + * @param string $text String to truncate. + * @param int $length Length of returned string, including ellipsis. + * @param array $options An array of HTML attributes and options. + * @return string Trimmed string. + * @see \Cake\Utility\Text::truncate() + */ + public static function truncateByWidth($text, $length = 100, array $options = []) + { + return static::truncate($text, $length, ['trimWidth' => true] + $options); + } + + /** + * Get string length. + * + * ### Options: + * + * - `html` If true, HTML entities will be handled as decoded characters. + * - `trimWidth` If true, the width will return. + * + * @param string $text The string being checked for length + * @param array $options An array of options. + * @return int + */ + protected static function _strlen($text, array $options) + { + if (empty($options['trimWidth'])) { + $strlen = 'mb_strlen'; + } else { + $strlen = 'mb_strwidth'; + } + + if (empty($options['html'])) { + return $strlen($text); + } + + $pattern = '/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i'; + $replace = preg_replace_callback( + $pattern, + function ($match) use ($strlen) { + $utf8 = html_entity_decode($match[0], ENT_HTML5 | ENT_QUOTES, 'UTF-8'); + + return str_repeat(' ', $strlen($utf8, 'UTF-8')); + }, + $text + ); + + return $strlen($replace); + } + + /** + * Return part of a string. + * + * ### Options: + * + * - `html` If true, HTML entities will be handled as decoded characters. + * - `trimWidth` If true, will be truncated with specified width. + * + * @param string $text The input string. + * @param int $start The position to begin extracting. + * @param int $length The desired length. + * @param array $options An array of options. + * @return string + */ + protected static function _substr($text, $start, $length, array $options) + { + if (empty($options['trimWidth'])) { + $substr = 'mb_substr'; + } else { + $substr = 'mb_strimwidth'; + } + + $maxPosition = self::_strlen($text, ['trimWidth' => false] + $options); + if ($start < 0) { + $start += $maxPosition; + if ($start < 0) { + $start = 0; + } + } + if ($start >= $maxPosition) { + return ''; + } + + if ($length === null) { + $length = self::_strlen($text, $options); + } + + if ($length < 0) { + $text = self::_substr($text, $start, null, $options); + $start = 0; + $length += self::_strlen($text, $options); + } + + if ($length <= 0) { + return ''; + } + + if (empty($options['html'])) { + return (string)$substr($text, $start, $length); + } + + $totalOffset = 0; + $totalLength = 0; + $result = ''; + + $pattern = '/(&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};)/i'; + $parts = preg_split($pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + foreach ($parts as $part) { + $offset = 0; + + if ($totalOffset < $start) { + $len = self::_strlen($part, ['trimWidth' => false] + $options); + if ($totalOffset + $len <= $start) { + $totalOffset += $len; + continue; + } + + $offset = $start - $totalOffset; + $totalOffset = $start; + } + + $len = self::_strlen($part, $options); + if ($offset !== 0 || $totalLength + $len > $length) { + if (strpos($part, '&') === 0 && preg_match($pattern, $part) + && $part !== html_entity_decode($part, ENT_HTML5 | ENT_QUOTES, 'UTF-8') + ) { + // Entities cannot be passed substr. + continue; + } + + $part = $substr($part, $offset, $length - $totalLength); + $len = self::_strlen($part, $options); + } + + $result .= $part; + $totalLength += $len; + if ($totalLength >= $length) { + break; + } + } + + return $result; + } + + /** + * Removes the last word from the input text. + * + * @param string $text The input text + * @return string + */ + protected static function _removeLastWord($text) + { + $spacepos = mb_strrpos($text, ' '); + + if ($spacepos !== false) { + $lastWord = mb_strrpos($text, $spacepos); + + // Some languages are written without word separation. + // We recognize a string as a word if it doesn't contain any full-width characters. + if (mb_strwidth($lastWord) === mb_strlen($lastWord)) { + $text = mb_substr($text, 0, $spacepos); + } + + return $text; + } + + return ''; + } + + /** + * Extracts an excerpt from the text surrounding the phrase with a number of characters on each side + * determined by radius. + * + * @param string $text String to search the phrase in + * @param string $phrase Phrase that will be searched for + * @param int $radius The amount of characters that will be returned on each side of the founded phrase + * @param string $ellipsis Ending that will be appended + * @return string Modified string + * @link https://book.cakephp.org/3.0/en/core-libraries/text.html#extracting-an-excerpt + */ + public static function excerpt($text, $phrase, $radius = 100, $ellipsis = '...') + { + if (empty($text) || empty($phrase)) { + return static::truncate($text, $radius * 2, ['ellipsis' => $ellipsis]); + } + + $append = $prepend = $ellipsis; + + $phraseLen = mb_strlen($phrase); + $textLen = mb_strlen($text); + + $pos = mb_stripos($text, $phrase); + if ($pos === false) { + return mb_substr($text, 0, $radius) . $ellipsis; + } + + $startPos = $pos - $radius; + if ($startPos <= 0) { + $startPos = 0; + $prepend = ''; + } + + $endPos = $pos + $phraseLen + $radius; + if ($endPos >= $textLen) { + $endPos = $textLen; + $append = ''; + } + + $excerpt = mb_substr($text, $startPos, $endPos - $startPos); + $excerpt = $prepend . $excerpt . $append; + + return $excerpt; + } + + /** + * Creates a comma separated list where the last two items are joined with 'and', forming natural language. + * + * @param array $list The list to be joined. + * @param string|null $and The word used to join the last and second last items together with. Defaults to 'and'. + * @param string $separator The separator used to join all the other items together. Defaults to ', '. + * @return string The glued together string. + * @link https://book.cakephp.org/3.0/en/core-libraries/text.html#converting-an-array-to-sentence-form + */ + public static function toList(array $list, $and = null, $separator = ', ') + { + if ($and === null) { + $and = __d('cake', 'and'); + } + if (count($list) > 1) { + return implode($separator, array_slice($list, null, -1)) . ' ' . $and . ' ' . array_pop($list); + } + + return array_pop($list); + } + + /** + * Check if the string contain multibyte characters + * + * @param string $string value to test + * @return bool + */ + public static function isMultibyte($string) + { + $length = strlen($string); + + for ($i = 0; $i < $length; $i++) { + $value = ord($string[$i]); + if ($value > 128) { + return true; + } + } + + return false; + } + + /** + * Converts a multibyte character string + * to the decimal value of the character + * + * @param string $string String to convert. + * @return array + */ + public static function utf8($string) + { + $map = []; + + $values = []; + $find = 1; + $length = strlen($string); + + for ($i = 0; $i < $length; $i++) { + $value = ord($string[$i]); + + if ($value < 128) { + $map[] = $value; + } else { + if (empty($values)) { + $find = ($value < 224) ? 2 : 3; + } + $values[] = $value; + + if (count($values) === $find) { + if ($find == 3) { + $map[] = (($values[0] % 16) * 4096) + (($values[1] % 64) * 64) + ($values[2] % 64); + } else { + $map[] = (($values[0] % 32) * 64) + ($values[1] % 64); + } + $values = []; + $find = 1; + } + } + } + + return $map; + } + + /** + * Converts the decimal value of a multibyte character string + * to a string + * + * @param array $array Array + * @return string + */ + public static function ascii(array $array) + { + $ascii = ''; + + foreach ($array as $utf8) { + if ($utf8 < 128) { + $ascii .= chr($utf8); + } elseif ($utf8 < 2048) { + $ascii .= chr(192 + (($utf8 - ($utf8 % 64)) / 64)); + $ascii .= chr(128 + ($utf8 % 64)); + } else { + $ascii .= chr(224 + (($utf8 - ($utf8 % 4096)) / 4096)); + $ascii .= chr(128 + ((($utf8 % 4096) - ($utf8 % 64)) / 64)); + $ascii .= chr(128 + ($utf8 % 64)); + } + } + + return $ascii; + } + + /** + * Converts filesize from human readable string to bytes + * + * @param string $size Size in human readable string like '5MB', '5M', '500B', '50kb' etc. + * @param mixed $default Value to be returned when invalid size was used, for example 'Unknown type' + * @return mixed Number of bytes as integer on success, `$default` on failure if not false + * @throws \InvalidArgumentException On invalid Unit type. + * @link https://book.cakephp.org/3.0/en/core-libraries/text.html#Cake\Utility\Text::parseFileSize + */ + public static function parseFileSize($size, $default = false) + { + if (ctype_digit($size)) { + return (int)$size; + } + $size = strtoupper($size); + + $l = -2; + $i = array_search(substr($size, -2), ['KB', 'MB', 'GB', 'TB', 'PB']); + if ($i === false) { + $l = -1; + $i = array_search(substr($size, -1), ['K', 'M', 'G', 'T', 'P']); + } + if ($i !== false) { + $size = substr($size, 0, $l); + + return $size * pow(1024, $i + 1); + } + + if (substr($size, -1) === 'B' && ctype_digit(substr($size, 0, -1))) { + $size = substr($size, 0, -1); + + return (int)$size; + } + + if ($default !== false) { + return $default; + } + throw new InvalidArgumentException('No unit type.'); + } + + /** + * Get default transliterator identifier string. + * + * @return string Transliterator identifier. + */ + public static function getTransliteratorId() + { + return static::$_defaultTransliteratorId; + } + + /** + * Set default transliterator identifier string. + * + * @param string $transliteratorId Transliterator identifier. + * @return void + */ + public static function setTransliteratorId($transliteratorId) + { + static::$_defaultTransliteratorId = $transliteratorId; + } + + /** + * Transliterate string. + * + * @param string $string String to transliterate. + * @param string|null $transliteratorId Transliterator identifier. If null + * Text::$_defaultTransliteratorId will be used. + * @return string + * @see https://secure.php.net/manual/en/transliterator.transliterate.php + */ + public static function transliterate($string, $transliteratorId = null) + { + $transliteratorId = $transliteratorId ?: static::$_defaultTransliteratorId; + + return transliterator_transliterate($transliteratorId, $string); + } + + /** + * Returns a string with all spaces converted to dashes (by default), + * characters transliterated to ASCII characters, and non word characters removed. + * + * ### Options: + * + * - `replacement`: Replacement string. Default '-'. + * - `transliteratorId`: A valid tranliterator id string. + * If default `null` Text::$_defaultTransliteratorId to be used. + * If `false` no transliteration will be done, only non words will be removed. + * - `preserve`: Specific non-word character to preserve. Default `null`. + * For e.g. this option can be set to '.' to generate clean file names. + * + * @param string $string the string you want to slug + * @param array $options If string it will be use as replacement character + * or an array of options. + * @return string + */ + public static function slug($string, $options = []) + { + if (is_string($options)) { + $options = ['replacement' => $options]; + } + $options += [ + 'replacement' => '-', + 'transliteratorId' => null, + 'preserve' => null + ]; + + if ($options['transliteratorId'] !== false) { + $string = static::transliterate($string, $options['transliteratorId']); + } + + $regex = '^\s\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}'; + if ($options['preserve']) { + $regex .= preg_quote($options['preserve'], '/'); + } + $quotedReplacement = preg_quote($options['replacement'], '/'); + $map = [ + '/[' . $regex . ']/mu' => ' ', + '/[\s]+/mu' => $options['replacement'], + sprintf('/^[%s]+|[%s]+$/', $quotedReplacement, $quotedReplacement) => '', + ]; + $string = preg_replace(array_keys($map), $map, $string); + + return $string; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Utility/Xml.php b/app/vendor/cakephp/cakephp/src/Utility/Xml.php new file mode 100644 index 000000000..01346d608 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Utility/Xml.php @@ -0,0 +1,441 @@ +text'); + * ``` + * + * Building XML from string (output DOMDocument): + * + * ``` + * $xml = Xml::build('text', ['return' => 'domdocument']); + * ``` + * + * Building XML from a file path: + * + * ``` + * $xml = Xml::build('/path/to/an/xml/file.xml'); + * ``` + * + * Building XML from a remote URL: + * + * ``` + * use Cake\Http\Client; + * + * $http = new Client(); + * $response = $http->get('http://example.com/example.xml'); + * $xml = Xml::build($response->body()); + * ``` + * + * Building from an array: + * + * ``` + * $value = [ + * 'tags' => [ + * 'tag' => [ + * [ + * 'id' => '1', + * 'name' => 'defect' + * ], + * [ + * 'id' => '2', + * 'name' => 'enhancement' + * ] + * ] + * ] + * ]; + * $xml = Xml::build($value); + * ``` + * + * When building XML from an array ensure that there is only one top level element. + * + * ### Options + * + * - `return` Can be 'simplexml' to return object of SimpleXMLElement or 'domdocument' to return DOMDocument. + * - `loadEntities` Defaults to false. Set to true to enable loading of ` 'simplexml', + 'loadEntities' => false, + 'readFile' => true, + 'parseHuge' => false, + ]; + $options += $defaults; + + if (is_array($input) || is_object($input)) { + return static::fromArray($input, $options); + } + + if (strpos($input, '<') !== false) { + return static::_loadXml($input, $options); + } + + if ($options['readFile'] && file_exists($input)) { + return static::_loadXml(file_get_contents($input), $options); + } + + if (!is_string($input)) { + throw new XmlException('Invalid input.'); + } + + throw new XmlException('XML cannot be read.'); + } + + /** + * Parse the input data and create either a SimpleXmlElement object or a DOMDocument. + * + * @param string $input The input to load. + * @param array $options The options to use. See Xml::build() + * @return \SimpleXMLElement|\DOMDocument + * @throws \Cake\Utility\Exception\XmlException + */ + protected static function _loadXml($input, $options) + { + $hasDisable = function_exists('libxml_disable_entity_loader'); + $internalErrors = libxml_use_internal_errors(true); + if ($hasDisable && !$options['loadEntities']) { + libxml_disable_entity_loader(true); + } + $flags = 0; + if (!empty($options['parseHuge'])) { + $flags |= LIBXML_PARSEHUGE; + } + try { + if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') { + $flags |= LIBXML_NOCDATA; + $xml = new SimpleXMLElement($input, $flags); + } else { + $xml = new DOMDocument(); + $xml->loadXML($input, $flags); + } + + return $xml; + } catch (Exception $e) { + throw new XmlException('Xml cannot be read. ' . $e->getMessage(), null, $e); + } finally { + if ($hasDisable && !$options['loadEntities']) { + libxml_disable_entity_loader(false); + } + libxml_use_internal_errors($internalErrors); + } + } + + /** + * Transform an array into a SimpleXMLElement + * + * ### Options + * + * - `format` If create childs ('tags') or attributes ('attributes'). + * - `pretty` Returns formatted Xml when set to `true`. Defaults to `false` + * - `version` Version of XML document. Default is 1.0. + * - `encoding` Encoding of XML document. If null remove from XML header. Default is the some of application. + * - `return` If return object of SimpleXMLElement ('simplexml') or DOMDocument ('domdocument'). Default is SimpleXMLElement. + * + * Using the following data: + * + * ``` + * $value = [ + * 'root' => [ + * 'tag' => [ + * 'id' => 1, + * 'value' => 'defect', + * '@' => 'description' + * ] + * ] + * ]; + * ``` + * + * Calling `Xml::fromArray($value, 'tags');` Will generate: + * + * `1defectdescription` + * + * And calling `Xml::fromArray($value, 'attributes');` Will generate: + * + * `description` + * + * @param array|\Cake\Collection\Collection $input Array with data or a collection instance. + * @param string|array $options The options to use or a string to use as format. + * @return \SimpleXMLElement|\DOMDocument SimpleXMLElement or DOMDocument + * @throws \Cake\Utility\Exception\XmlException + */ + public static function fromArray($input, $options = []) + { + if (is_object($input) && method_exists($input, 'toArray') && is_callable([$input, 'toArray'])) { + $input = call_user_func([$input, 'toArray']); + } + if (!is_array($input) || count($input) !== 1) { + throw new XmlException('Invalid input.'); + } + $key = key($input); + if (is_int($key)) { + throw new XmlException('The key of input must be alphanumeric'); + } + + if (!is_array($options)) { + $options = ['format' => (string)$options]; + } + $defaults = [ + 'format' => 'tags', + 'version' => '1.0', + 'encoding' => mb_internal_encoding(), + 'return' => 'simplexml', + 'pretty' => false + ]; + $options += $defaults; + + $dom = new DOMDocument($options['version'], $options['encoding']); + if ($options['pretty']) { + $dom->formatOutput = true; + } + self::_fromArray($dom, $dom, $input, $options['format']); + + $options['return'] = strtolower($options['return']); + if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') { + return new SimpleXMLElement($dom->saveXML()); + } + + return $dom; + } + + /** + * Recursive method to create childs from array + * + * @param \DOMDocument $dom Handler to DOMDocument + * @param \DOMElement $node Handler to DOMElement (child) + * @param array $data Array of data to append to the $node. + * @param string $format Either 'attributes' or 'tags'. This determines where nested keys go. + * @return void + * @throws \Cake\Utility\Exception\XmlException + */ + protected static function _fromArray($dom, $node, &$data, $format) + { + if (empty($data) || !is_array($data)) { + return; + } + foreach ($data as $key => $value) { + if (is_string($key)) { + if (is_object($value) && method_exists($value, 'toArray') && is_callable([$value, 'toArray'])) { + $value = call_user_func([$value, 'toArray']); + } + + if (!is_array($value)) { + if (is_bool($value)) { + $value = (int)$value; + } elseif ($value === null) { + $value = ''; + } + $isNamespace = strpos($key, 'xmlns:'); + if ($isNamespace !== false) { + $node->setAttributeNS('http://www.w3.org/2000/xmlns/', $key, $value); + continue; + } + if ($key[0] !== '@' && $format === 'tags') { + if (!is_numeric($value)) { + // Escape special characters + // https://www.w3.org/TR/REC-xml/#syntax + // https://bugs.php.net/bug.php?id=36795 + $child = $dom->createElement($key, ''); + $child->appendChild(new DOMText($value)); + } else { + $child = $dom->createElement($key, $value); + } + $node->appendChild($child); + } else { + if ($key[0] === '@') { + $key = substr($key, 1); + } + $attribute = $dom->createAttribute($key); + $attribute->appendChild($dom->createTextNode($value)); + $node->appendChild($attribute); + } + } else { + if ($key[0] === '@') { + throw new XmlException('Invalid array'); + } + if (is_numeric(implode('', array_keys($value)))) { +// List + foreach ($value as $item) { + $itemData = compact('dom', 'node', 'key', 'format'); + $itemData['value'] = $item; + static::_createChild($itemData); + } + } else { +// Struct + static::_createChild(compact('dom', 'node', 'key', 'value', 'format')); + } + } + } else { + throw new XmlException('Invalid array'); + } + } + } + + /** + * Helper to _fromArray(). It will create childs of arrays + * + * @param array $data Array with information to create childs + * @return void + */ + protected static function _createChild($data) + { + $data += [ + 'dom' => null, + 'node' => null, + 'key' => null, + 'value' => null, + 'format' => null, + ]; + + $value = $data['value']; + $dom = $data['dom']; + $key = $data['key']; + $format = $data['format']; + $node = $data['node']; + + $childNS = $childValue = null; + if (is_object($value) && method_exists($value, 'toArray') && is_callable([$value, 'toArray'])) { + $value = call_user_func([$value, 'toArray']); + } + if (is_array($value)) { + if (isset($value['@'])) { + $childValue = (string)$value['@']; + unset($value['@']); + } + if (isset($value['xmlns:'])) { + $childNS = $value['xmlns:']; + unset($value['xmlns:']); + } + } elseif (!empty($value) || $value === 0 || $value === '0') { + $childValue = (string)$value; + } + + $child = $dom->createElement($key); + if ($childValue !== null) { + $child->appendChild($dom->createTextNode($childValue)); + } + if ($childNS) { + $child->setAttribute('xmlns', $childNS); + } + + static::_fromArray($dom, $child, $value, $format); + $node->appendChild($child); + } + + /** + * Returns this XML structure as an array. + * + * @param \SimpleXMLElement|\DOMDocument|\DOMNode $obj SimpleXMLElement, DOMDocument or DOMNode instance + * @return array Array representation of the XML structure. + * @throws \Cake\Utility\Exception\XmlException + */ + public static function toArray($obj) + { + if ($obj instanceof DOMNode) { + $obj = simplexml_import_dom($obj); + } + if (!($obj instanceof SimpleXMLElement)) { + throw new XmlException('The input is not instance of SimpleXMLElement, DOMDocument or DOMNode.'); + } + $result = []; + $namespaces = array_merge(['' => ''], $obj->getNamespaces(true)); + static::_toArray($obj, $result, '', array_keys($namespaces)); + + return $result; + } + + /** + * Recursive method to toArray + * + * @param \SimpleXMLElement $xml SimpleXMLElement object + * @param array $parentData Parent array with data + * @param string $ns Namespace of current child + * @param array $namespaces List of namespaces in XML + * @return void + */ + protected static function _toArray($xml, &$parentData, $ns, $namespaces) + { + $data = []; + + foreach ($namespaces as $namespace) { + foreach ($xml->attributes($namespace, true) as $key => $value) { + if (!empty($namespace)) { + $key = $namespace . ':' . $key; + } + $data['@' . $key] = (string)$value; + } + + foreach ($xml->children($namespace, true) as $child) { + static::_toArray($child, $data, $namespace, $namespaces); + } + } + + $asString = trim((string)$xml); + if (empty($data)) { + $data = $asString; + } elseif (strlen($asString) > 0) { + $data['@'] = $asString; + } + + if (!empty($ns)) { + $ns .= ':'; + } + $name = $ns . $xml->getName(); + if (isset($parentData[$name])) { + if (!is_array($parentData[$name]) || !isset($parentData[$name][0])) { + $parentData[$name] = [$parentData[$name]]; + } + $parentData[$name][] = $data; + } else { + $parentData[$name] = $data; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Utility/bootstrap.php b/app/vendor/cakephp/cakephp/src/Utility/bootstrap.php new file mode 100644 index 000000000..fbc847175 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Utility/bootstrap.php @@ -0,0 +1,18 @@ +=5.6.0", + "cakephp/core": "^3.6.0" + }, + "suggest": { + "ext-intl": "To use Text::transliterate() or Text::slug()", + "lib-ICU": "To use Text::transliterate() or Text::slug()" + }, + "autoload": { + "psr-4": { + "Cake\\Utility\\": "." + }, + "files": [ + "bootstrap.php" + ] + } +} diff --git a/app/vendor/cakephp/cakephp/src/Validation/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Validation/LICENSE.txt new file mode 100644 index 000000000..0c4b7932c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Validation/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) +Copyright (c) 2005-2016, Cake Software Foundation, Inc. (https://cakefoundation.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/vendor/cakephp/cakephp/src/Validation/README.md b/app/vendor/cakephp/cakephp/src/Validation/README.md new file mode 100644 index 000000000..179665169 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Validation/README.md @@ -0,0 +1,37 @@ +[![Total Downloads](https://img.shields.io/packagist/dt/cakephp/validation.svg?style=flat-square)](https://packagist.org/packages/cakephp/validation) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) + +# CakePHP Validation Library + +The validation library in CakePHP provides features to build validators that can validate arbitrary +arrays of data with ease. + +## Usage + +Validator objects define the rules that apply to a set of fields. Validator objects contain a mapping between +fields and validation sets. Creating a validator is simple: + +```php +use Cake\Validation\Validator; + +$validator = new Validator(); +$validator + ->requirePresence('email') + ->add('email', 'validFormat', [ + 'rule' => 'email', + 'message' => 'E-mail must be valid' + ]) + ->requirePresence('name') + ->notEmpty('name', 'We need your name.') + ->requirePresence('comment') + ->notEmpty('comment', 'You need to give a comment.'); + +$errors = $validator->errors($_POST); +if (!empty($errors)) { + // display errors. +} +``` + +## Documentation + +Please make sure you check the [official documentation](https://book.cakephp.org/3.0/en/core-libraries/validation.html) diff --git a/app/vendor/cakephp/cakephp/src/Validation/RulesProvider.php b/app/vendor/cakephp/cakephp/src/Validation/RulesProvider.php new file mode 100644 index 000000000..efdee0db5 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Validation/RulesProvider.php @@ -0,0 +1,75 @@ +_class = $class; + $this->_reflection = new ReflectionClass($class); + } + + /** + * Proxies validation method calls to the Validation class. + * + * The last argument (context) will be sliced off, if the validation + * method's last parameter is not named 'context'. This lets + * the various wrapped validation methods to not receive the validation + * context unless they need it. + * + * @param string $method the validation method to call + * @param array $arguments the list of arguments to pass to the method + * @return bool whether or not the validation rule passed + */ + public function __call($method, $arguments) + { + $method = $this->_reflection->getMethod($method); + $argumentList = $method->getParameters(); + if (array_pop($argumentList)->getName() !== 'context') { + $arguments = array_slice($arguments, 0, -1); + } + $object = is_string($this->_class) ? null : $this->_class; + + return $method->invokeArgs($object, $arguments); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Validation/ValidatableInterface.php b/app/vendor/cakephp/cakephp/src/Validation/ValidatableInterface.php new file mode 100644 index 000000000..35780823c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Validation/ValidatableInterface.php @@ -0,0 +1,31 @@ +'; + + /** + * Greater than or equal to comparison operator. + */ + const COMPARE_GREATER_OR_EQUAL = '>='; + + /** + * Less than comparison operator. + */ + const COMPARE_LESS = '<'; + + /** + * Less than or equal to comparison operator. + */ + const COMPARE_LESS_OR_EQUAL = '<='; + + /** + * Some complex patterns needed in multiple places + * + * @var array + */ + protected static $_pattern = [ + 'hostname' => '(?:[_\p{L}0-9][-_\p{L}0-9]*\.)*(?:[\p{L}0-9][-\p{L}0-9]{0,62})\.(?:(?:[a-z]{2}\.)?[a-z]{2,})', + 'latitude' => '[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)', + 'longitude' => '[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)', + ]; + + /** + * Holds an array of errors messages set in this class. + * These are used for debugging purposes + * + * @var array + */ + public static $errors = []; + + /** + * Backwards compatibility wrapper for Validation::notBlank(). + * + * @param string $check Value to check. + * @return bool Success. + * @deprecated 3.0.2 Use Validation::notBlank() instead. + * @see \Cake\Validation\Validation::notBlank() + */ + public static function notEmpty($check) + { + deprecationWarning( + 'Validation::notEmpty() is deprecated. ' . + 'Use Validation::notBlank() instead.' + ); + + return static::notBlank($check); + } + + /** + * Checks that a string contains something other than whitespace + * + * Returns true if string contains something other than whitespace + * + * @param string $check Value to check + * @return bool Success + */ + public static function notBlank($check) + { + if (empty($check) && !is_bool($check) && !is_numeric($check)) { + return false; + } + + return static::_check($check, '/[^\s]+/m'); + } + + /** + * Checks that a string contains only integer or letters + * + * Returns true if string contains only integer or letters + * + * @param string $check Value to check + * @return bool Success + */ + public static function alphaNumeric($check) + { + if (empty($check) && $check !== '0') { + return false; + } + + return self::_check($check, '/^[\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}]+$/Du'); + } + + /** + * Checks that a string length is within specified range. + * Spaces are included in the character count. + * Returns true if string matches value min, max, or between min and max, + * + * @param string $check Value to check for length + * @param int $min Minimum value in range (inclusive) + * @param int $max Maximum value in range (inclusive) + * @return bool Success + */ + public static function lengthBetween($check, $min, $max) + { + if (!is_string($check)) { + return false; + } + $length = mb_strlen($check); + + return ($length >= $min && $length <= $max); + } + + /** + * Returns true if field is left blank -OR- only whitespace characters are present in its value + * Whitespace characters include Space, Tab, Carriage Return, Newline + * + * @param string $check Value to check + * @return bool Success + * @deprecated 3.0.2 Validation::blank() is deprecated. + */ + public static function blank($check) + { + deprecationWarning( + 'Validation::blank() is deprecated.' + ); + + return !static::_check($check, '/[^\\s]/'); + } + + /** + * Validation of credit card numbers. + * Returns true if $check is in the proper credit card format. + * + * @param string $check credit card number to validate + * @param string|array $type 'all' may be passed as a string, defaults to fast which checks format of most major credit cards + * if an array is used only the values of the array are checked. + * Example: ['amex', 'bankcard', 'maestro'] + * @param bool $deep set to true this will check the Luhn algorithm of the credit card. + * @param string|null $regex A custom regex can also be passed, this will be used instead of the defined regex values + * @return bool Success + * @see \Cake\Validation\Validation::luhn() + */ + public static function cc($check, $type = 'fast', $deep = false, $regex = null) + { + if (!is_scalar($check)) { + return false; + } + + $check = str_replace(['-', ' '], '', $check); + if (mb_strlen($check) < 13) { + return false; + } + + if ($regex !== null && static::_check($check, $regex)) { + return !$deep || static::luhn($check); + } + $cards = [ + 'all' => [ + 'amex' => '/^3[47]\\d{13}$/', + 'bankcard' => '/^56(10\\d\\d|022[1-5])\\d{10}$/', + 'diners' => '/^(?:3(0[0-5]|[68]\\d)\\d{11})|(?:5[1-5]\\d{14})$/', + 'disc' => '/^(?:6011|650\\d)\\d{12}$/', + 'electron' => '/^(?:417500|4917\\d{2}|4913\\d{2})\\d{10}$/', + 'enroute' => '/^2(?:014|149)\\d{11}$/', + 'jcb' => '/^(3\\d{4}|2131|1800)\\d{11}$/', + 'maestro' => '/^(?:5020|6\\d{3})\\d{12}$/', + 'mc' => '/^(5[1-5]\\d{14})|(2(?:22[1-9]|2[3-9][0-9]|[3-6][0-9]{2}|7[0-1][0-9]|720)\\d{12})$/', + 'solo' => '/^(6334[5-9][0-9]|6767[0-9]{2})\\d{10}(\\d{2,3})?$/', + 'switch' => '/^(?:49(03(0[2-9]|3[5-9])|11(0[1-2]|7[4-9]|8[1-2])|36[0-9]{2})\\d{10}(\\d{2,3})?)|(?:564182\\d{10}(\\d{2,3})?)|(6(3(33[0-4][0-9])|759[0-9]{2})\\d{10}(\\d{2,3})?)$/', + 'visa' => '/^4\\d{12}(\\d{3})?$/', + 'voyager' => '/^8699[0-9]{11}$/' + ], + 'fast' => '/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6011[0-9]{12}|3(?:0[0-5]|[68][0-9])[0-9]{11}|3[47][0-9]{13})$/' + ]; + + if (is_array($type)) { + foreach ($type as $value) { + $regex = $cards['all'][strtolower($value)]; + + if (static::_check($check, $regex)) { + return static::luhn($check); + } + } + } elseif ($type === 'all') { + foreach ($cards['all'] as $value) { + $regex = $value; + + if (static::_check($check, $regex)) { + return static::luhn($check); + } + } + } else { + $regex = $cards['fast']; + + if (static::_check($check, $regex)) { + return static::luhn($check); + } + } + + return false; + } + + /** + * Used to check the count of a given value of type array or Countable. + * + * @param array|\Countable $check The value to check the count on. + * @param string $operator Can be either a word or operand + * is greater >, is less <, greater or equal >= + * less or equal <=, is less <, equal to ==, not equal != + * @param int $expectedCount The expected count value. + * @return bool Success + */ + public static function numElements($check, $operator, $expectedCount) + { + if (!is_array($check) && !$check instanceof \Countable) { + return false; + } + + return self::comparison(count($check), $operator, $expectedCount); + } + + /** + * Used to compare 2 numeric values. + * + * @param string $check1 The left value to compare. + * @param string $operator Can be either a word or operand + * is greater >, is less <, greater or equal >= + * less or equal <=, is less <, equal to ==, not equal != + * @param int $check2 The right value to compare. + * @return bool Success + */ + public static function comparison($check1, $operator, $check2) + { + if ((float)$check1 != $check1) { + return false; + } + + $message = 'Operator `%s` is deprecated, use constant `Validation::%s` instead.'; + + $operator = str_replace([' ', "\t", "\n", "\r", "\0", "\x0B"], '', strtolower($operator)); + switch ($operator) { + case 'isgreater': + /* + * @deprecated 3.6.0 Use Validation::COMPARE_GREATER instead. + */ + deprecationWarning(sprintf($message, $operator, 'COMPARE_GREATER')); + // no break + case static::COMPARE_GREATER: + if ($check1 > $check2) { + return true; + } + break; + case 'isless': + /* + * @deprecated 3.6.0 Use Validation::COMPARE_LESS instead. + */ + deprecationWarning(sprintf($message, $operator, 'COMPARE_LESS')); + // no break + case static::COMPARE_LESS: + if ($check1 < $check2) { + return true; + } + break; + case 'greaterorequal': + /* + * @deprecated 3.6.0 Use Validation::COMPARE_GREATER_OR_EQUAL instead. + */ + deprecationWarning(sprintf($message, $operator, 'COMPARE_GREATER_OR_EQUAL')); + // no break + case static::COMPARE_GREATER_OR_EQUAL: + if ($check1 >= $check2) { + return true; + } + break; + case 'lessorequal': + /* + * @deprecated 3.6.0 Use Validation::COMPARE_LESS_OR_EQUAL instead. + */ + deprecationWarning(sprintf($message, $operator, 'COMPARE_LESS_OR_EQUAL')); + // no break + case static::COMPARE_LESS_OR_EQUAL: + if ($check1 <= $check2) { + return true; + } + break; + case 'equalto': + /* + * @deprecated 3.6.0 Use Validation::COMPARE_EQUAL instead. + */ + deprecationWarning(sprintf($message, $operator, 'COMPARE_EQUAL')); + // no break + case static::COMPARE_EQUAL: + if ($check1 == $check2) { + return true; + } + break; + case 'notequal': + /* + * @deprecated 3.6.0 Use Validation::COMPARE_NOT_EQUAL instead. + */ + deprecationWarning(sprintf($message, $operator, 'COMPARE_NOT_EQUAL')); + // no break + case static::COMPARE_NOT_EQUAL: + if ($check1 != $check2) { + return true; + } + break; + case static::COMPARE_SAME: + if ($check1 === $check2) { + return true; + } + break; + case static::COMPARE_NOT_SAME: + if ($check1 !== $check2) { + return true; + } + break; + default: + static::$errors[] = 'You must define the $operator parameter for Validation::comparison()'; + } + + return false; + } + + /** + * Compare one field to another. + * + * If both fields have exactly the same value this method will return true. + * + * @param mixed $check The value to find in $field. + * @param string $field The field to check $check against. This field must be present in $context. + * @param array $context The validation context. + * @return bool + */ + public static function compareWith($check, $field, $context) + { + return self::compareFields($check, $field, static::COMPARE_SAME, $context); + } + + /** + * Compare one field to another. + * + * Return true if the comparison matches the expected result. + * + * @param mixed $check The value to find in $field. + * @param string $field The field to check $check against. This field must be present in $context. + * @param string $operator Comparison operator. + * @param array $context The validation context. + * @return bool + * @since 3.6.0 + */ + public static function compareFields($check, $field, $operator, $context) + { + if (!isset($context['data'][$field])) { + return false; + } + + return static::comparison($check, $operator, $context['data'][$field]); + } + + /** + * Checks if a string contains one or more non-alphanumeric characters. + * + * Returns true if string contains at least the specified number of non-alphanumeric characters + * + * @param string $check Value to check + * @param int $count Number of non-alphanumerics to check for + * @return bool Success + */ + public static function containsNonAlphaNumeric($check, $count = 1) + { + if (!is_scalar($check)) { + return false; + } + + $matches = preg_match_all('/[^a-zA-Z0-9]/', $check); + + return $matches >= $count; + } + + /** + * Used when a custom regular expression is needed. + * + * @param string $check The value to check. + * @param string|null $regex If $check is passed as a string, $regex must also be set to valid regular expression + * @return bool Success + */ + public static function custom($check, $regex = null) + { + if ($regex === null) { + static::$errors[] = 'You must define a regular expression for Validation::custom()'; + + return false; + } + + return static::_check($check, $regex); + } + + /** + * Date validation, determines if the string passed is a valid date. + * keys that expect full month, day and year will validate leap years. + * + * Years are valid from 1800 to 2999. + * + * ### Formats: + * + * - `dmy` 27-12-2006 or 27-12-06 separators can be a space, period, dash, forward slash + * - `mdy` 12-27-2006 or 12-27-06 separators can be a space, period, dash, forward slash + * - `ymd` 2006-12-27 or 06-12-27 separators can be a space, period, dash, forward slash + * - `dMy` 27 December 2006 or 27 Dec 2006 + * - `Mdy` December 27, 2006 or Dec 27, 2006 comma is optional + * - `My` December 2006 or Dec 2006 + * - `my` 12/2006 or 12/06 separators can be a space, period, dash, forward slash + * - `ym` 2006/12 or 06/12 separators can be a space, period, dash, forward slash + * - `y` 2006 just the year without any separators + * + * @param string|\DateTimeInterface $check a valid date string/object + * @param string|array $format Use a string or an array of the keys above. + * Arrays should be passed as ['dmy', 'mdy', etc] + * @param string|null $regex If a custom regular expression is used this is the only validation that will occur. + * @return bool Success + */ + public static function date($check, $format = 'ymd', $regex = null) + { + if ($check instanceof DateTimeInterface) { + return true; + } + if (is_object($check)) { + return false; + } + if (is_array($check)) { + $check = static::_getDateString($check); + $format = 'ymd'; + } + + if ($regex !== null) { + return static::_check($check, $regex); + } + $month = '(0[123456789]|10|11|12)'; + $separator = '([- /.])'; + $fourDigitYear = '(([1][8-9][0-9][0-9])|([2][0-9][0-9][0-9]))'; + $twoDigitYear = '([0-9]{2})'; + $year = '(?:' . $fourDigitYear . '|' . $twoDigitYear . ')'; + + $regex['dmy'] = '%^(?:(?:31(\\/|-|\\.|\\x20)(?:0?[13578]|1[02]))\\1|(?:(?:29|30)' . + $separator . '(?:0?[1,3-9]|1[0-2])\\2))(?:(?:1[6-9]|[2-9]\\d)?\\d{2})$|^(?:29' . + $separator . '0?2\\3(?:(?:(?:1[6-9]|[2-9]\\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\\d|2[0-8])' . + $separator . '(?:(?:0?[1-9])|(?:1[0-2]))\\4(?:(?:1[6-9]|[2-9]\\d)?\\d{2})$%'; + + $regex['mdy'] = '%^(?:(?:(?:0?[13578]|1[02])(\\/|-|\\.|\\x20)31)\\1|(?:(?:0?[13-9]|1[0-2])' . + $separator . '(?:29|30)\\2))(?:(?:1[6-9]|[2-9]\\d)?\\d{2})$|^(?:0?2' . $separator . '29\\3(?:(?:(?:1[6-9]|[2-9]\\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:(?:0?[1-9])|(?:1[0-2]))' . + $separator . '(?:0?[1-9]|1\\d|2[0-8])\\4(?:(?:1[6-9]|[2-9]\\d)?\\d{2})$%'; + + $regex['ymd'] = '%^(?:(?:(?:(?:(?:1[6-9]|[2-9]\\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00)))' . + $separator . '(?:0?2\\1(?:29)))|(?:(?:(?:1[6-9]|[2-9]\\d)?\\d{2})' . + $separator . '(?:(?:(?:0?[13578]|1[02])\\2(?:31))|(?:(?:0?[1,3-9]|1[0-2])\\2(29|30))|(?:(?:0?[1-9])|(?:1[0-2]))\\2(?:0?[1-9]|1\\d|2[0-8]))))$%'; + + $regex['dMy'] = '/^((31(?!\\ (Feb(ruary)?|Apr(il)?|June?|(Sep(?=\\b|t)t?|Nov)(ember)?)))|((30|29)(?!\\ Feb(ruary)?))|(29(?=\\ Feb(ruary)?\\ (((1[6-9]|[2-9]\\d)(0[48]|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00)))))|(0?[1-9])|1\\d|2[0-8])\\ (Jan(uary)?|Feb(ruary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)|(ne?))|Aug(ust)?|Oct(ober)?|(Sep(?=\\b|t)t?|Nov|Dec)(ember)?)\\ ((1[6-9]|[2-9]\\d)\\d{2})$/'; + + $regex['Mdy'] = '/^(?:(((Jan(uary)?|Ma(r(ch)?|y)|Jul(y)?|Aug(ust)?|Oct(ober)?|Dec(ember)?)\\ 31)|((Jan(uary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)|(ne?))|Aug(ust)?|Oct(ober)?|(Sep)(tember)?|(Nov|Dec)(ember)?)\\ (0?[1-9]|([12]\\d)|30))|(Feb(ruary)?\\ (0?[1-9]|1\\d|2[0-8]|(29(?=,?\\ ((1[6-9]|[2-9]\\d)(0[48]|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00)))))))\\,?\\ ((1[6-9]|[2-9]\\d)\\d{2}))$/'; + + $regex['My'] = '%^(Jan(uary)?|Feb(ruary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)|(ne?))|Aug(ust)?|Oct(ober)?|(Sep(?=\\b|t)t?|Nov|Dec)(ember)?)' . + $separator . '((1[6-9]|[2-9]\\d)\\d{2})$%'; + + $regex['my'] = '%^(' . $month . $separator . $year . ')$%'; + $regex['ym'] = '%^(' . $year . $separator . $month . ')$%'; + $regex['y'] = '%^(' . $fourDigitYear . ')$%'; + + $format = is_array($format) ? array_values($format) : [$format]; + foreach ($format as $key) { + if (static::_check($check, $regex[$key]) === true) { + return true; + } + } + + return false; + } + + /** + * Validates a datetime value + * + * All values matching the "date" core validation rule, and the "time" one will be valid + * + * @param string|\DateTimeInterface $check Value to check + * @param string|array $dateFormat Format of the date part. See Validation::date() for more information. + * @param string|null $regex Regex for the date part. If a custom regular expression is used this is the only validation that will occur. + * @return bool True if the value is valid, false otherwise + * @see \Cake\Validation\Validation::date() + * @see \Cake\Validation\Validation::time() + */ + public static function datetime($check, $dateFormat = 'ymd', $regex = null) + { + if ($check instanceof DateTimeInterface) { + return true; + } + if (is_object($check)) { + return false; + } + $valid = false; + if (is_array($check)) { + $check = static::_getDateString($check); + $dateFormat = 'ymd'; + } + $parts = explode(' ', $check); + if (!empty($parts) && count($parts) > 1) { + $date = rtrim(array_shift($parts), ','); + $time = implode(' ', $parts); + $valid = static::date($date, $dateFormat, $regex) && static::time($time); + } + + return $valid; + } + + /** + * Time validation, determines if the string passed is a valid time. + * Validates time as 24hr (HH:MM) or am/pm ([H]H:MM[a|p]m) + * Does not allow/validate seconds. + * + * @param string|\DateTimeInterface $check a valid time string/object + * @return bool Success + */ + public static function time($check) + { + if ($check instanceof DateTimeInterface) { + return true; + } + if (is_array($check)) { + $check = static::_getDateString($check); + } + + return static::_check($check, '%^((0?[1-9]|1[012])(:[0-5]\d){0,2} ?([AP]M|[ap]m))$|^([01]\d|2[0-3])(:[0-5]\d){0,2}$%'); + } + + /** + * Date and/or time string validation. + * Uses `I18n::Time` to parse the date. This means parsing is locale dependent. + * + * @param string|\DateTime $check a date string or object (will always pass) + * @param string $type Parser type, one out of 'date', 'time', and 'datetime' + * @param string|int|null $format any format accepted by IntlDateFormatter + * @return bool Success + * @throws \InvalidArgumentException when unsupported $type given + * @see \Cake\I18n\Time::parseDate(), \Cake\I18n\Time::parseTime(), \Cake\I18n\Time::parseDateTime() + */ + public static function localizedTime($check, $type = 'datetime', $format = null) + { + if ($check instanceof DateTimeInterface) { + return true; + } + if (is_object($check)) { + return false; + } + static $methods = [ + 'date' => 'parseDate', + 'time' => 'parseTime', + 'datetime' => 'parseDateTime', + ]; + if (empty($methods[$type])) { + throw new InvalidArgumentException('Unsupported parser type given.'); + } + $method = $methods[$type]; + + return (Time::$method($check, $format) !== null); + } + + /** + * Validates if passed value is boolean-like. + * + * The list of what is considered to be boolean values, may be set via $booleanValues. + * + * @param bool|int|string $check Value to check. + * @param array $booleanValues List of valid boolean values, defaults to `[true, false, 0, 1, '0', '1']`. + * @return bool Success. + */ + public static function boolean($check, array $booleanValues = []) + { + if (!$booleanValues) { + $booleanValues = [true, false, 0, 1, '0', '1']; + } + + return in_array($check, $booleanValues, true); + } + + /** + * Validates if given value is truthy. + * + * The list of what is considered to be truthy values, may be set via $truthyValues. + * + * @param bool|int|string $check Value to check. + * @param array $truthyValues List of valid truthy values, defaults to `[true, 1, '1']`. + * @return bool Success. + */ + public static function truthy($check, array $truthyValues = []) + { + if (!$truthyValues) { + $truthyValues = [true, 1, '1']; + } + + return in_array($check, $truthyValues, true); + } + + /** + * Validates if given value is falsey. + * + * The list of what is considered to be falsey values, may be set via $falseyValues. + * + * @param bool|int|string $check Value to check. + * @param array $falseyValues List of valid falsey values, defaults to `[false, 0, '0']`. + * @return bool Success. + */ + public static function falsey($check, array $falseyValues = []) + { + if (!$falseyValues) { + $falseyValues = [false, 0, '0']; + } + + return in_array($check, $falseyValues, true); + } + + /** + * Checks that a value is a valid decimal. Both the sign and exponent are optional. + * + * Valid Places: + * + * - null => Any number of decimal places, including none. The '.' is not required. + * - true => Any number of decimal places greater than 0, or a float|double. The '.' is required. + * - 1..N => Exactly that many number of decimal places. The '.' is required. + * + * @param float $check The value the test for decimal. + * @param int|bool|null $places Decimal places. + * @param string|null $regex If a custom regular expression is used, this is the only validation that will occur. + * @return bool Success + */ + public static function decimal($check, $places = null, $regex = null) + { + if ($regex === null) { + $lnum = '[0-9]+'; + $dnum = "[0-9]*[\.]{$lnum}"; + $sign = '[+-]?'; + $exp = "(?:[eE]{$sign}{$lnum})?"; + + if ($places === null) { + $regex = "/^{$sign}(?:{$lnum}|{$dnum}){$exp}$/"; + } elseif ($places === true) { + if (is_float($check) && floor($check) === $check) { + $check = sprintf('%.1f', $check); + } + $regex = "/^{$sign}{$dnum}{$exp}$/"; + } elseif (is_numeric($places)) { + $places = '[0-9]{' . $places . '}'; + $dnum = "(?:[0-9]*[\.]{$places}|{$lnum}[\.]{$places})"; + $regex = "/^{$sign}{$dnum}{$exp}$/"; + } + } + + // account for localized floats. + $locale = ini_get('intl.default_locale') ?: static::DEFAULT_LOCALE; + $formatter = new NumberFormatter($locale, NumberFormatter::DECIMAL); + $decimalPoint = $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); + $groupingSep = $formatter->getSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL); + + $check = str_replace([$groupingSep, $decimalPoint], ['', '.'], $check); + + return static::_check($check, $regex); + } + + /** + * Validates for an email address. + * + * Only uses getmxrr() checking for deep validation, or + * any PHP version on a non-windows distribution + * + * @param string $check Value to check + * @param bool $deep Perform a deeper validation (if true), by also checking availability of host + * @param string|null $regex Regex to use (if none it will use built in regex) + * @return bool Success + */ + public static function email($check, $deep = false, $regex = null) + { + if (!is_string($check)) { + return false; + } + + if ($regex === null) { + $regex = '/^[\p{L}0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[\p{L}0-9!#$%&\'*+\/=?^_`{|}~-]+)*@' . self::$_pattern['hostname'] . '$/ui'; + } + $return = static::_check($check, $regex); + if ($deep === false || $deep === null) { + return $return; + } + + if ($return === true && preg_match('/@(' . static::$_pattern['hostname'] . ')$/i', $check, $regs)) { + if (function_exists('getmxrr') && getmxrr($regs[1], $mxhosts)) { + return true; + } + if (function_exists('checkdnsrr') && checkdnsrr($regs[1], 'MX')) { + return true; + } + + return is_array(gethostbynamel($regs[1] . '.')); + } + + return false; + } + + /** + * Checks that value is exactly $comparedTo. + * + * @param mixed $check Value to check + * @param mixed $comparedTo Value to compare + * @return bool Success + */ + public static function equalTo($check, $comparedTo) + { + return ($check === $comparedTo); + } + + /** + * Checks that value has a valid file extension. + * + * @param string|array $check Value to check + * @param array $extensions file extensions to allow. By default extensions are 'gif', 'jpeg', 'png', 'jpg' + * @return bool Success + */ + public static function extension($check, $extensions = ['gif', 'jpeg', 'png', 'jpg']) + { + if (is_array($check)) { + $check = isset($check['name']) ? $check['name'] : array_shift($check); + + return static::extension($check, $extensions); + } + $extension = strtolower(pathinfo($check, PATHINFO_EXTENSION)); + foreach ($extensions as $value) { + if ($extension === strtolower($value)) { + return true; + } + } + + return false; + } + + /** + * Validation of an IP address. + * + * @param string $check The string to test. + * @param string $type The IP Protocol version to validate against + * @return bool Success + */ + public static function ip($check, $type = 'both') + { + $type = strtolower($type); + $flags = 0; + if ($type === 'ipv4') { + $flags = FILTER_FLAG_IPV4; + } + if ($type === 'ipv6') { + $flags = FILTER_FLAG_IPV6; + } + + return (bool)filter_var($check, FILTER_VALIDATE_IP, ['flags' => $flags]); + } + + /** + * Checks whether the length of a string (in characters) is greater or equal to a minimal length. + * + * @param string $check The string to test + * @param int $min The minimal string length + * @return bool Success + */ + public static function minLength($check, $min) + { + return mb_strlen($check) >= $min; + } + + /** + * Checks whether the length of a string (in characters) is smaller or equal to a maximal length. + * + * @param string $check The string to test + * @param int $max The maximal string length + * @return bool Success + */ + public static function maxLength($check, $max) + { + return mb_strlen($check) <= $max; + } + + /** + * Checks whether the length of a string (in bytes) is greater or equal to a minimal length. + * + * @param string $check The string to test + * @param int $min The minimal string length (in bytes) + * @return bool Success + */ + public static function minLengthBytes($check, $min) + { + return strlen($check) >= $min; + } + + /** + * Checks whether the length of a string (in bytes) is smaller or equal to a maximal length. + * + * @param string $check The string to test + * @param int $max The maximal string length + * @return bool Success + */ + public static function maxLengthBytes($check, $max) + { + return strlen($check) <= $max; + } + + /** + * Checks that a value is a monetary amount. + * + * @param string $check Value to check + * @param string $symbolPosition Where symbol is located (left/right) + * @return bool Success + */ + public static function money($check, $symbolPosition = 'left') + { + $money = '(?!0,?\d)(?:\d{1,3}(?:([, .])\d{3})?(?:\1\d{3})*|(?:\d+))((?!\1)[,.]\d{1,2})?'; + if ($symbolPosition === 'right') { + $regex = '/^' . $money . '(? provide a list of choices that selections must be made from + * - max => maximum number of non-zero choices that can be made + * - min => minimum number of non-zero choices that can be made + * + * @param array $check Value to check + * @param array $options Options for the check. + * @param bool $caseInsensitive Set to true for case insensitive comparison. + * @return bool Success + */ + public static function multiple($check, array $options = [], $caseInsensitive = false) + { + $defaults = ['in' => null, 'max' => null, 'min' => null]; + $options += $defaults; + + $check = array_filter((array)$check, function ($value) { + return ($value || is_numeric($value)); + }); + if (empty($check)) { + return false; + } + if ($options['max'] && count($check) > $options['max']) { + return false; + } + if ($options['min'] && count($check) < $options['min']) { + return false; + } + if ($options['in'] && is_array($options['in'])) { + if ($caseInsensitive) { + $options['in'] = array_map('mb_strtolower', $options['in']); + } + foreach ($check as $val) { + $strict = !is_numeric($val); + if ($caseInsensitive) { + $val = mb_strtolower($val); + } + if (!in_array((string)$val, $options['in'], $strict)) { + return false; + } + } + } + + return true; + } + + /** + * Checks if a value is numeric. + * + * @param string $check Value to check + * @return bool Success + */ + public static function numeric($check) + { + return is_numeric($check); + } + + /** + * Checks if a value is a natural number. + * + * @param string $check Value to check + * @param bool $allowZero Set true to allow zero, defaults to false + * @return bool Success + * @see https://en.wikipedia.org/wiki/Natural_number + */ + public static function naturalNumber($check, $allowZero = false) + { + $regex = $allowZero ? '/^(?:0|[1-9][0-9]*)$/' : '/^[1-9][0-9]*$/'; + + return static::_check($check, $regex); + } + + /** + * Validates that a number is in specified range. + * + * If $lower and $upper are set, the range is inclusive. + * If they are not set, will return true if $check is a + * legal finite on this platform. + * + * @param string $check Value to check + * @param int|float|null $lower Lower limit + * @param int|float|null $upper Upper limit + * @return bool Success + */ + public static function range($check, $lower = null, $upper = null) + { + if (!is_numeric($check)) { + return false; + } + if ((float)$check != $check) { + return false; + } + if (isset($lower, $upper)) { + return ($check >= $lower && $check <= $upper); + } + + return is_finite($check); + } + + /** + * Checks that a value is a valid URL according to https://www.w3.org/Addressing/URL/url-spec.txt + * + * The regex checks for the following component parts: + * + * - a valid, optional, scheme + * - a valid ip address OR + * a valid domain name as defined by section 2.3.1 of https://www.ietf.org/rfc/rfc1035.txt + * with an optional port number + * - an optional valid path + * - an optional query string (get parameters) + * - an optional fragment (anchor tag) as defined in RFC 3986 + * + * @param string $check Value to check + * @param bool $strict Require URL to be prefixed by a valid scheme (one of http(s)/ftp(s)/file/news/gopher) + * @return bool Success + * @link https://tools.ietf.org/html/rfc3986 + */ + public static function url($check, $strict = false) + { + static::_populateIp(); + + $emoji = '\x{1F190}-\x{1F9EF}'; + $alpha = '0-9\p{L}\p{N}' . $emoji; + $hex = '(%[0-9a-f]{2})'; + $subDelimiters = preg_quote('/!"$&\'()*+,-.@_:;=~[]', '/'); + $path = '([' . $subDelimiters . $alpha . ']|' . $hex . ')'; + $fragmentAndQuery = '([\?' . $subDelimiters . $alpha . ']|' . $hex . ')'; + $regex = '/^(?:(?:https?|ftps?|sftp|file|news|gopher):\/\/)' . (!empty($strict) ? '' : '?') . + '(?:' . static::$_pattern['IPv4'] . '|\[' . static::$_pattern['IPv6'] . '\]|' . static::$_pattern['hostname'] . ')(?::[1-9][0-9]{0,4})?' . + '(?:\/' . $path . '*)?' . + '(?:\?' . $fragmentAndQuery . '*)?' . + '(?:#' . $fragmentAndQuery . '*)?$/iu'; + + return static::_check($check, $regex); + } + + /** + * Checks if a value is in a given list. Comparison is case sensitive by default. + * + * @param string $check Value to check. + * @param array $list List to check against. + * @param bool $caseInsensitive Set to true for case insensitive comparison. + * @return bool Success. + */ + public static function inList($check, array $list, $caseInsensitive = false) + { + if ($caseInsensitive) { + $list = array_map('mb_strtolower', $list); + $check = mb_strtolower($check); + } else { + $list = array_map('strval', $list); + } + + return in_array((string)$check, $list, true); + } + + /** + * Runs an user-defined validation. + * + * @param string|array $check value that will be validated in user-defined methods. + * @param object $object class that holds validation method + * @param string $method class method name for validation to run + * @param array|null $args arguments to send to method + * @return mixed user-defined class class method returns + * @deprecated 3.0.2 You can just set a callable for `rule` key when adding validators. + */ + public static function userDefined($check, $object, $method, $args = null) + { + deprecationWarning( + 'Validation::userDefined() is deprecated. ' . + 'You can just set a callable for `rule` key when adding validators.' + ); + + return $object->$method($check, $args); + } + + /** + * Checks that a value is a valid UUID - https://tools.ietf.org/html/rfc4122 + * + * @param string $check Value to check + * @return bool Success + */ + public static function uuid($check) + { + $regex = '/^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[0-5][a-fA-F0-9]{3}-[089aAbB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$/'; + + return self::_check($check, $regex); + } + + /** + * Runs a regular expression match. + * + * @param string $check Value to check against the $regex expression + * @param string $regex Regular expression + * @return bool Success of match + */ + protected static function _check($check, $regex) + { + return is_string($regex) && is_scalar($check) && preg_match($regex, $check); + } + + /** + * Luhn algorithm + * + * @param string|array $check Value to check. + * @return bool Success + * @see https://en.wikipedia.org/wiki/Luhn_algorithm + */ + public static function luhn($check) + { + if (!is_scalar($check) || (int)$check === 0) { + return false; + } + $sum = 0; + $length = strlen($check); + + for ($position = 1 - ($length % 2); $position < $length; $position += 2) { + $sum += $check[$position]; + } + + for ($position = ($length % 2); $position < $length; $position += 2) { + $number = $check[$position] * 2; + $sum += ($number < 10) ? $number : $number - 9; + } + + return ($sum % 10 === 0); + } + + /** + * Checks the mime type of a file. + * + * Will check the mimetype of files/UploadedFileInterface instances + * by checking the using finfo on the file, not relying on the content-type + * sent by the client. + * + * @param string|array|\Psr\Http\Message\UploadedFileInterface $check Value to check. + * @param array|string $mimeTypes Array of mime types or regex pattern to check. + * @return bool Success + * @throws \RuntimeException when mime type can not be determined. + * @throws \LogicException when ext/fileinfo is missing + */ + public static function mimeType($check, $mimeTypes = []) + { + $file = static::getFilename($check); + if ($file === false) { + return false; + } + + if (!function_exists('finfo_open')) { + throw new LogicException('ext/fileinfo is required for validating file mime types'); + } + + if (!is_file($file)) { + throw new RuntimeException('Cannot validate mimetype for a missing file'); + } + + $finfo = finfo_open(FILEINFO_MIME); + $finfo = finfo_file($finfo, $file); + + if (!$finfo) { + throw new RuntimeException('Can not determine the mimetype.'); + } + + list($mime) = explode(';', $finfo); + + if (is_string($mimeTypes)) { + return self::_check($mime, $mimeTypes); + } + + foreach ($mimeTypes as $key => $val) { + $mimeTypes[$key] = strtolower($val); + } + + return in_array($mime, $mimeTypes); + } + + /** + * Helper for reading the file out of the various file implementations + * we accept. + * + * @param string|array|\Psr\Http\Message\UploadedFileInterface $check The data to read a filename out of. + * @return string|bool Either the filename or false on failure. + */ + protected static function getFilename($check) + { + if ($check instanceof UploadedFileInterface) { + try { + // Uploaded files throw exceptions on upload errors. + return $check->getStream()->getMetadata('uri'); + } catch (RuntimeException $e) { + return false; + } + } + if (is_array($check) && isset($check['tmp_name'])) { + return $check['tmp_name']; + } + + if (is_string($check)) { + return $check; + } + + return false; + } + + /** + * Checks the filesize + * + * Will check the filesize of files/UploadedFileInterface instances + * by checking the filesize() on disk and not relying on the length + * reported by the client. + * + * @param string|array|\Psr\Http\Message\UploadedFileInterface $check Value to check. + * @param string|null $operator See `Validation::comparison()`. + * @param int|string|null $size Size in bytes or human readable string like '5MB'. + * @return bool Success + */ + public static function fileSize($check, $operator = null, $size = null) + { + $file = static::getFilename($check); + if ($file === false) { + return false; + } + + if (is_string($size)) { + $size = Text::parseFileSize($size); + } + $filesize = filesize($file); + + return static::comparison($filesize, $operator, $size); + } + + /** + * Checking for upload errors + * + * @param string|array|\Psr\Http\Message\UploadedFileInterface $check Value to check. + * @param bool $allowNoFile Set to true to allow UPLOAD_ERR_NO_FILE as a pass. + * @return bool + * @see https://secure.php.net/manual/en/features.file-upload.errors.php + */ + public static function uploadError($check, $allowNoFile = false) + { + if ($check instanceof UploadedFileInterface) { + $code = $check->getError(); + } elseif (is_array($check) && isset($check['error'])) { + $code = $check['error']; + } else { + $code = $check; + } + if ($allowNoFile) { + return in_array((int)$code, [UPLOAD_ERR_OK, UPLOAD_ERR_NO_FILE], true); + } + + return (int)$code === UPLOAD_ERR_OK; + } + + /** + * Validate an uploaded file. + * + * Helps join `uploadError`, `fileSize` and `mimeType` into + * one higher level validation method. + * + * ### Options + * + * - `types` - An array of valid mime types. If empty all types + * will be accepted. The `type` will not be looked at, instead + * the file type will be checked with ext/finfo. + * - `minSize` - The minimum file size in bytes. Defaults to not checking. + * - `maxSize` - The maximum file size in bytes. Defaults to not checking. + * - `optional` - Whether or not this file is optional. Defaults to false. + * If true a missing file will pass the validator regardless of other constraints. + * + * @param array $file The uploaded file data from PHP. + * @param array $options An array of options for the validation. + * @return bool + */ + public static function uploadedFile($file, array $options = []) + { + $options += [ + 'minSize' => null, + 'maxSize' => null, + 'types' => null, + 'optional' => false, + ]; + if (!is_array($file) && !($file instanceof UploadedFileInterface)) { + return false; + } + $error = $isUploaded = false; + if ($file instanceof UploadedFileInterface) { + $error = $file->getError(); + $isUploaded = true; + } + if (is_array($file)) { + $keys = ['error', 'name', 'size', 'tmp_name', 'type']; + ksort($file); + if (array_keys($file) != $keys) { + return false; + } + $error = (int)$file['error']; + $isUploaded = is_uploaded_file($file['tmp_name']); + } + + if (!static::uploadError($file, $options['optional'])) { + return false; + } + if ($options['optional'] && $error === UPLOAD_ERR_NO_FILE) { + return true; + } + if (isset($options['minSize']) && !static::fileSize($file, static::COMPARE_GREATER_OR_EQUAL, $options['minSize'])) { + return false; + } + if (isset($options['maxSize']) && !static::fileSize($file, static::COMPARE_LESS_OR_EQUAL, $options['maxSize'])) { + return false; + } + if (isset($options['types']) && !static::mimeType($file, $options['types'])) { + return false; + } + + return $isUploaded; + } + + /** + * Validates the size of an uploaded image. + * + * @param array $file The uploaded file data from PHP. + * @param array $options Options to validate width and height. + * @return bool + */ + public static function imageSize($file, $options) + { + if (!isset($options['height']) && !isset($options['width'])) { + throw new InvalidArgumentException('Invalid image size validation parameters! Missing `width` and / or `height`.'); + } + + if ($file instanceof UploadedFileInterface) { + $file = $file->getStream()->getContents(); + } elseif (is_array($file) && isset($file['tmp_name'])) { + $file = $file['tmp_name']; + } + + list($width, $height) = getimagesize($file); + + if (isset($options['height'])) { + $validHeight = self::comparison($height, $options['height'][0], $options['height'][1]); + } + if (isset($options['width'])) { + $validWidth = self::comparison($width, $options['width'][0], $options['width'][1]); + } + if (isset($validHeight, $validWidth)) { + return ($validHeight && $validWidth); + } + if (isset($validHeight)) { + return $validHeight; + } + if (isset($validWidth)) { + return $validWidth; + } + + throw new InvalidArgumentException('The 2nd argument is missing the `width` and / or `height` options.'); + } + + /** + * Validates the image width. + * + * @param array $file The uploaded file data from PHP. + * @param string $operator Comparision operator. + * @param int $width Min or max width. + * @return bool + */ + public static function imageWidth($file, $operator, $width) + { + return self::imageSize($file, [ + 'width' => [ + $operator, + $width + ] + ]); + } + + /** + * Validates the image width. + * + * @param array $file The uploaded file data from PHP. + * @param string $operator Comparision operator. + * @param int $height Min or max width. + * @return bool + */ + public static function imageHeight($file, $operator, $height) + { + return self::imageSize($file, [ + 'height' => [ + $operator, + $height + ] + ]); + } + + /** + * Validates a geographic coordinate. + * + * Supported formats: + * + * - `, ` Example: `-25.274398, 133.775136` + * + * ### Options + * + * - `type` - A string of the coordinate format, right now only `latLong`. + * - `format` - By default `both`, can be `long` and `lat` as well to validate + * only a part of the coordinate. + * + * @param string $value Geographic location as string + * @param array $options Options for the validation logic. + * @return bool + */ + public static function geoCoordinate($value, array $options = []) + { + $options += [ + 'format' => 'both', + 'type' => 'latLong' + ]; + if ($options['type'] !== 'latLong') { + throw new RuntimeException(sprintf( + 'Unsupported coordinate type "%s". Use "latLong" instead.', + $options['type'] + )); + } + $pattern = '/^' . self::$_pattern['latitude'] . ',\s*' . self::$_pattern['longitude'] . '$/'; + if ($options['format'] === 'long') { + $pattern = '/^' . self::$_pattern['longitude'] . '$/'; + } + if ($options['format'] === 'lat') { + $pattern = '/^' . self::$_pattern['latitude'] . '$/'; + } + + return (bool)preg_match($pattern, $value); + } + + /** + * Convenience method for latitude validation. + * + * @param string $value Latitude as string + * @param array $options Options for the validation logic. + * @return bool + * @link https://en.wikipedia.org/wiki/Latitude + * @see \Cake\Validation\Validation::geoCoordinate() + */ + public static function latitude($value, array $options = []) + { + $options['format'] = 'lat'; + + return self::geoCoordinate($value, $options); + } + + /** + * Convenience method for longitude validation. + * + * @param string $value Latitude as string + * @param array $options Options for the validation logic. + * @return bool + * @link https://en.wikipedia.org/wiki/Longitude + * @see \Cake\Validation\Validation::geoCoordinate() + */ + public static function longitude($value, array $options = []) + { + $options['format'] = 'long'; + + return self::geoCoordinate($value, $options); + } + + /** + * Check that the input value is within the ascii byte range. + * + * This method will reject all non-string values. + * + * @param string $value The value to check + * @return bool + */ + public static function ascii($value) + { + if (!is_string($value)) { + return false; + } + + return strlen($value) <= mb_strlen($value, 'utf-8'); + } + + /** + * Check that the input value is a utf8 string. + * + * This method will reject all non-string values. + * + * # Options + * + * - `extended` - Disallow bytes higher within the basic multilingual plane. + * MySQL's older utf8 encoding type does not allow characters above + * the basic multilingual plane. Defaults to false. + * + * @param string $value The value to check + * @param array $options An array of options. See above for the supported options. + * @return bool + */ + public static function utf8($value, array $options = []) + { + if (!is_string($value)) { + return false; + } + $options += ['extended' => false]; + if ($options['extended']) { + return true; + } + + return preg_match('/[\x{10000}-\x{10FFFF}]/u', $value) === 0; + } + + /** + * Check that the input value is an integer + * + * This method will accept strings that contain only integer data + * as well. + * + * @param string $value The value to check + * @return bool + */ + public static function isInteger($value) + { + if (!is_scalar($value) || is_float($value)) { + return false; + } + if (is_int($value)) { + return true; + } + + return (bool)preg_match('/^-?[0-9]+$/', $value); + } + + /** + * Check that the input value is an array. + * + * @param array $value The value to check + * @return bool + */ + public static function isArray($value) + { + return is_array($value); + } + + /** + * Check that the input value is a scalar. + * + * This method will accept integers, floats, strings and booleans, but + * not accept arrays, objects, resources and nulls. + * + * @param mixed $value The value to check + * @return bool + */ + public static function isScalar($value) + { + return is_scalar($value); + } + + /** + * Check that the input value is a 6 digits hex color. + * + * @param string|array $check The value to check + * @return bool Success + */ + public static function hexColor($check) + { + return static::_check($check, '/^#[0-9a-f]{6}$/iD'); + } + + /** + * Converts an array representing a date or datetime into a ISO string. + * The arrays are typically sent for validation from a form generated by + * the CakePHP FormHelper. + * + * @param array $value The array representing a date or datetime. + * @return string + */ + protected static function _getDateString($value) + { + $formatted = ''; + if (isset($value['year'], $value['month'], $value['day']) && + (is_numeric($value['year']) && is_numeric($value['month']) && is_numeric($value['day'])) + ) { + $formatted .= sprintf('%d-%02d-%02d ', $value['year'], $value['month'], $value['day']); + } + + if (isset($value['hour'])) { + if (isset($value['meridian']) && (int)$value['hour'] === 12) { + $value['hour'] = 0; + } + if (isset($value['meridian'])) { + $value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12; + } + $value += ['minute' => 0, 'second' => 0]; + if (is_numeric($value['hour']) && is_numeric($value['minute']) && is_numeric($value['second'])) { + $formatted .= sprintf('%02d:%02d:%02d', $value['hour'], $value['minute'], $value['second']); + } + } + + return trim($formatted); + } + + /** + * Lazily populate the IP address patterns used for validations + * + * @return void + */ + protected static function _populateIp() + { + if (!isset(static::$_pattern['IPv6'])) { + $pattern = '((([0-9A-Fa-f]{1,4}:){7}(([0-9A-Fa-f]{1,4})|:))|(([0-9A-Fa-f]{1,4}:){6}'; + $pattern .= '(:|((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})'; + $pattern .= '|(:[0-9A-Fa-f]{1,4})))|(([0-9A-Fa-f]{1,4}:){5}((:((25[0-5]|2[0-4]\d|[01]?\d{1,2})'; + $pattern .= '(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|((:[0-9A-Fa-f]{1,4}){1,2})))|(([0-9A-Fa-f]{1,4}:)'; + $pattern .= '{4}(:[0-9A-Fa-f]{1,4}){0,1}((:((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2}))'; + $pattern .= '{3})?)|((:[0-9A-Fa-f]{1,4}){1,2})))|(([0-9A-Fa-f]{1,4}:){3}(:[0-9A-Fa-f]{1,4}){0,2}'; + $pattern .= '((:((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|'; + $pattern .= '((:[0-9A-Fa-f]{1,4}){1,2})))|(([0-9A-Fa-f]{1,4}:){2}(:[0-9A-Fa-f]{1,4}){0,3}'; + $pattern .= '((:((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2}))'; + $pattern .= '{3})?)|((:[0-9A-Fa-f]{1,4}){1,2})))|(([0-9A-Fa-f]{1,4}:)(:[0-9A-Fa-f]{1,4})'; + $pattern .= '{0,4}((:((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)'; + $pattern .= '|((:[0-9A-Fa-f]{1,4}){1,2})))|(:(:[0-9A-Fa-f]{1,4}){0,5}((:((25[0-5]|2[0-4]'; + $pattern .= '\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|((:[0-9A-Fa-f]{1,4})'; + $pattern .= '{1,2})))|(((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})))(%.+)?'; + + static::$_pattern['IPv6'] = $pattern; + } + if (!isset(static::$_pattern['IPv4'])) { + $pattern = '(?:(?:25[0-5]|2[0-4][0-9]|(?:(?:1[0-9])?|[1-9]?)[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|(?:(?:1[0-9])?|[1-9]?)[0-9])'; + static::$_pattern['IPv4'] = $pattern; + } + } + + /** + * Reset internal variables for another validation run. + * + * @return void + */ + protected static function _reset() + { + static::$errors = []; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Validation/ValidationRule.php b/app/vendor/cakephp/cakephp/src/Validation/ValidationRule.php new file mode 100644 index 000000000..e5dd4e5bf --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Validation/ValidationRule.php @@ -0,0 +1,215 @@ +_addValidatorProps($validator); + } + + /** + * Returns whether this rule should break validation process for associated field + * after it fails + * + * @return bool + */ + public function isLast() + { + return (bool)$this->_last; + } + + /** + * Dispatches the validation rule to the given validator method and returns + * a boolean indicating whether the rule passed or not. If a string is returned + * it is assumed that the rule failed and the error message was given as a result. + * + * @param mixed $value The data to validate + * @param array $providers associative array with objects or class names that will + * be passed as the last argument for the validation method + * @param array $context A key value list of data that could be used as context + * during validation. Recognized keys are: + * - newRecord: (boolean) whether or not the data to be validated belongs to a + * new record + * - data: The full data that was passed to the validation process + * - field: The name of the field that is being processed + * @return bool|string + * @throws \InvalidArgumentException when the supplied rule is not a valid + * callable for the configured scope + */ + public function process($value, array $providers, array $context = []) + { + $context += ['data' => [], 'newRecord' => true, 'providers' => $providers]; + + if ($this->_skip($context)) { + return true; + } + + if (!is_string($this->_rule) && is_callable($this->_rule)) { + $callable = $this->_rule; + $isCallable = true; + } else { + $provider = $providers[$this->_provider]; + $callable = [$provider, $this->_rule]; + $isCallable = is_callable($callable); + } + + if (!$isCallable) { + $message = 'Unable to call method "%s" in "%s" provider for field "%s"'; + throw new InvalidArgumentException( + sprintf($message, $this->_rule, $this->_provider, $context['field']) + ); + } + + if ($this->_pass) { + $args = array_values(array_merge([$value], $this->_pass, [$context])); + $result = $callable(...$args); + } else { + $result = $callable($value, $context); + } + + if ($result === false) { + return $this->_message ?: false; + } + + return $result; + } + + /** + * Checks if the validation rule should be skipped + * + * @param array $context A key value list of data that could be used as context + * during validation. Recognized keys are: + * - newRecord: (boolean) whether or not the data to be validated belongs to a + * new record + * - data: The full data that was passed to the validation process + * - providers associative array with objects or class names that will + * be passed as the last argument for the validation method + * @return bool True if the ValidationRule should be skipped + */ + protected function _skip($context) + { + if (!is_string($this->_on) && is_callable($this->_on)) { + $function = $this->_on; + + return !$function($context); + } + + $newRecord = $context['newRecord']; + if (!empty($this->_on)) { + if (($this->_on === 'create' && !$newRecord) || ($this->_on === 'update' && $newRecord)) { + return true; + } + } + + return false; + } + + /** + * Sets the rule properties from the rule entry in validate + * + * @param array $validator [optional] + * @return void + */ + protected function _addValidatorProps($validator = []) + { + foreach ($validator as $key => $value) { + if (!isset($value) || empty($value)) { + continue; + } + if ($key === 'rule' && is_array($value) && !is_callable($value)) { + $this->_pass = array_slice($value, 1); + $value = array_shift($value); + } + if (in_array($key, ['rule', 'on', 'message', 'last', 'provider', 'pass'])) { + $this->{"_$key"} = $value; + } + } + } + + /** + * Returns the value of a property by name + * + * @param string $property The name of the property to retrieve. + * @return mixed + */ + public function get($property) + { + $property = '_' . $property; + if (isset($this->{$property})) { + return $this->{$property}; + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/Validation/ValidationSet.php b/app/vendor/cakephp/cakephp/src/Validation/ValidationSet.php new file mode 100644 index 000000000..de4aa4446 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Validation/ValidationSet.php @@ -0,0 +1,256 @@ +_validatePresent; + } + + deprecationWarning( + 'ValidationSet::isPresenceRequired() is deprecated as a setter. ' . + 'Use ValidationSet::requirePresence() instead.' + ); + + return $this->requirePresence($validatePresent); + } + + /** + * Sets whether a field is required to be present in data array. + * + * @param bool|string|callable $validatePresent Valid values are true, false, 'create', 'update' or a callable. + * @return $this + */ + public function requirePresence($validatePresent) + { + $this->_validatePresent = $validatePresent; + + return $this; + } + + /** + * Sets whether a field value is allowed to be empty. + * + * If no argument is passed the currently set `allowEmpty` value will be returned. + * + * @param bool|string|callable|null $allowEmpty Deprecated since 3.6.0 ValidationSet::isEmptyAllowed() is deprecated as a setter. + * Use ValidationSet::allowEmpty() instead. + * @return bool|string|callable + */ + public function isEmptyAllowed($allowEmpty = null) + { + if ($allowEmpty === null) { + return $this->_allowEmpty; + } + + deprecationWarning( + 'ValidationSet::isEmptyAllowed() is deprecated as a setter. ' . + 'Use ValidationSet::allowEmpty() instead.' + ); + + return $this->allowEmpty($allowEmpty); + } + + /** + * Sets whether a field value is allowed to be empty. + * + * @param bool|string|callable $allowEmpty Valid values are true, false, + * 'create', 'update' or a callable. + * @return $this + */ + public function allowEmpty($allowEmpty) + { + $this->_allowEmpty = $allowEmpty; + + return $this; + } + + /** + * Gets a rule for a given name if exists + * + * @param string $name The name under which the rule is set. + * @return \Cake\Validation\ValidationRule|null + */ + public function rule($name) + { + if (!empty($this->_rules[$name])) { + return $this->_rules[$name]; + } + } + + /** + * Returns all rules for this validation set + * + * @return \Cake\Validation\ValidationRule[] + */ + public function rules() + { + return $this->_rules; + } + + /** + * Sets a ValidationRule $rule with a $name + * + * ### Example: + * + * ``` + * $set + * ->add('notBlank', ['rule' => 'notBlank']) + * ->add('inRange', ['rule' => ['between', 4, 10]) + * ``` + * + * @param string $name The name under which the rule should be set + * @param \Cake\Validation\ValidationRule|array $rule The validation rule to be set + * @return $this + */ + public function add($name, $rule) + { + if (!($rule instanceof ValidationRule)) { + $rule = new ValidationRule($rule); + } + $this->_rules[$name] = $rule; + + return $this; + } + + /** + * Removes a validation rule from the set + * + * ### Example: + * + * ``` + * $set + * ->remove('notBlank') + * ->remove('inRange') + * ``` + * + * @param string $name The name under which the rule should be unset + * @return $this + */ + public function remove($name) + { + unset($this->_rules[$name]); + + return $this; + } + + /** + * Returns whether an index exists in the rule set + * + * @param string $index name of the rule + * @return bool + */ + public function offsetExists($index) + { + return isset($this->_rules[$index]); + } + + /** + * Returns a rule object by its index + * + * @param string $index name of the rule + * @return \Cake\Validation\ValidationRule + */ + public function offsetGet($index) + { + return $this->_rules[$index]; + } + + /** + * Sets or replace a validation rule + * + * @param string $index name of the rule + * @param \Cake\Validation\ValidationRule|array $rule Rule to add to $index + * @return void + */ + public function offsetSet($index, $rule) + { + $this->add($index, $rule); + } + + /** + * Unsets a validation rule + * + * @param string $index name of the rule + * @return void + */ + public function offsetUnset($index) + { + unset($this->_rules[$index]); + } + + /** + * Returns an iterator for each of the rules to be applied + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->_rules); + } + + /** + * Returns the number of rules in this set + * + * @return int + */ + public function count() + { + return count($this->_rules); + } +} diff --git a/app/vendor/cakephp/cakephp/src/Validation/Validator.php b/app/vendor/cakephp/cakephp/src/Validation/Validator.php new file mode 100644 index 000000000..7d6c23cd7 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Validation/Validator.php @@ -0,0 +1,2103 @@ +_useI18n = function_exists('__d'); + $this->_providers = self::$_defaultProviders; + } + + /** + * Returns an array of fields that have failed validation. On the current model. This method will + * actually run validation rules over data, not just return the messages. + * + * @param array $data The data to be checked for errors + * @param bool $newRecord whether the data to be validated is new or to be updated. + * @return array Array of invalid fields + */ + public function errors(array $data, $newRecord = true) + { + $errors = []; + + $requiredMessage = 'This field is required'; + $emptyMessage = 'This field cannot be left empty'; + + if ($this->_useI18n) { + $requiredMessage = __d('cake', 'This field is required'); + $emptyMessage = __d('cake', 'This field cannot be left empty'); + } + + foreach ($this->_fields as $name => $field) { + $keyPresent = array_key_exists($name, $data); + + $providers = $this->_providers; + $context = compact('data', 'newRecord', 'field', 'providers'); + + if (!$keyPresent && !$this->_checkPresence($field, $context)) { + $errors[$name]['_required'] = isset($this->_presenceMessages[$name]) + ? $this->_presenceMessages[$name] + : $requiredMessage; + continue; + } + if (!$keyPresent) { + continue; + } + + $canBeEmpty = $this->_canBeEmpty($field, $context); + $isEmpty = $this->_fieldIsEmpty($data[$name]); + + if (!$canBeEmpty && $isEmpty) { + $errors[$name]['_empty'] = isset($this->_allowEmptyMessages[$name]) + ? $this->_allowEmptyMessages[$name] + : $emptyMessage; + continue; + } + + if ($isEmpty) { + continue; + } + + $result = $this->_processRules($name, $field, $data, $newRecord); + if ($result) { + $errors[$name] = $result; + } + } + + return $errors; + } + + /** + * Returns a ValidationSet object containing all validation rules for a field, if + * passed a ValidationSet as second argument, it will replace any other rule set defined + * before + * + * @param string $name [optional] The fieldname to fetch. + * @param \Cake\Validation\ValidationSet|null $set The set of rules for field + * @return \Cake\Validation\ValidationSet + */ + public function field($name, ValidationSet $set = null) + { + if (empty($this->_fields[$name])) { + $set = $set ?: new ValidationSet(); + $this->_fields[$name] = $set; + } + + return $this->_fields[$name]; + } + + /** + * Check whether or not a validator contains any rules for the given field. + * + * @param string $name The field name to check. + * @return bool + */ + public function hasField($name) + { + return isset($this->_fields[$name]); + } + + /** + * Associates an object to a name so it can be used as a provider. Providers are + * objects or class names that can contain methods used during validation of for + * deciding whether a validation rule can be applied. All validation methods, + * when called will receive the full list of providers stored in this validator. + * + * @param string $name The name under which the provider should be set. + * @param object|string $object Provider object or class name. + * @return $this + */ + public function setProvider($name, $object) + { + $this->_providers[$name] = $object; + + return $this; + } + + /** + * Returns the provider stored under that name if it exists. + * + * @param string $name The name under which the provider should be set. + * @return object|string|null + * @throws \ReflectionException + */ + public function getProvider($name) + { + if (isset($this->_providers[$name])) { + return $this->_providers[$name]; + } + if ($name !== 'default') { + return null; + } + + $this->_providers[$name] = new RulesProvider(); + + return $this->_providers[$name]; + } + + /** + * Returns the default provider stored under that name if it exists. + * + * @param string $name The name under which the provider should be retrieved. + * @return object|string|null + */ + public static function getDefaultProvider($name) + { + if (!isset(self::$_defaultProviders[$name])) { + return null; + } + + return self::$_defaultProviders[$name]; + } + + /** + * Associates an object to a name so it can be used as a default provider. + * + * @param string $name The name under which the provider should be set. + * @param object|string $object Provider object or class name. + * @return void + */ + public static function addDefaultProvider($name, $object) + { + self::$_defaultProviders[$name] = $object; + } + + /** + * Get the list of default providers. + * + * @return array + */ + public static function getDefaultProviders() + { + return array_keys(self::$_defaultProviders); + } + + /** + * Associates an object to a name so it can be used as a provider. Providers are + * objects or class names that can contain methods used during validation of for + * deciding whether a validation rule can be applied. All validation methods, + * when called will receive the full list of providers stored in this validator. + * + * If called with no arguments, it will return the provider stored under that name if + * it exists, otherwise it returns this instance of chaining. + * + * @deprecated 3.4.0 Use setProvider()/getProvider() instead. + * @param string $name The name under which the provider should be set. + * @param null|object|string $object Provider object or class name. + * @return $this|object|string|null + */ + public function provider($name, $object = null) + { + deprecationWarning( + 'Validator::provider() is deprecated. ' . + 'Use Validator::setProvider()/getProvider() instead.' + ); + if ($object !== null) { + return $this->setProvider($name, $object); + } + + return $this->getProvider($name); + } + + /** + * Get the list of providers in this validator. + * + * @return array + */ + public function providers() + { + return array_keys($this->_providers); + } + + /** + * Returns whether a rule set is defined for a field or not + * + * @param string $field name of the field to check + * @return bool + */ + public function offsetExists($field) + { + return isset($this->_fields[$field]); + } + + /** + * Returns the rule set for a field + * + * @param string $field name of the field to check + * @return \Cake\Validation\ValidationSet + */ + public function offsetGet($field) + { + return $this->field($field); + } + + /** + * Sets the rule set for a field + * + * @param string $field name of the field to set + * @param array|\Cake\Validation\ValidationSet $rules set of rules to apply to field + * @return void + */ + public function offsetSet($field, $rules) + { + if (!$rules instanceof ValidationSet) { + $set = new ValidationSet(); + foreach ((array)$rules as $name => $rule) { + $set->add($name, $rule); + } + } + $this->_fields[$field] = $rules; + } + + /** + * Unsets the rule set for a field + * + * @param string $field name of the field to unset + * @return void + */ + public function offsetUnset($field) + { + unset($this->_fields[$field]); + } + + /** + * Returns an iterator for each of the fields to be validated + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->_fields); + } + + /** + * Returns the number of fields having validation rules + * + * @return int + */ + public function count() + { + return count($this->_fields); + } + + /** + * Adds a new rule to a field's rule set. If second argument is an array + * then rules list for the field will be replaced with second argument and + * third argument will be ignored. + * + * ### Example: + * + * ``` + * $validator + * ->add('title', 'required', ['rule' => 'notBlank']) + * ->add('user_id', 'valid', ['rule' => 'numeric', 'message' => 'Invalid User']) + * + * $validator->add('password', [ + * 'size' => ['rule' => ['lengthBetween', 8, 20]], + * 'hasSpecialCharacter' => ['rule' => 'validateSpecialchar', 'message' => 'not valid'] + * ]); + * ``` + * + * @param string $field The name of the field from which the rule will be added + * @param array|string $name The alias for a single rule or multiple rules array + * @param array|\Cake\Validation\ValidationRule $rule the rule to add + * @return $this + */ + public function add($field, $name, $rule = []) + { + $field = $this->field($field); + + if (!is_array($name)) { + $rules = [$name => $rule]; + } else { + $rules = $name; + } + + foreach ($rules as $name => $rule) { + if (is_array($rule)) { + $rule += ['rule' => $name]; + } + $field->add($name, $rule); + } + + return $this; + } + + /** + * Adds a nested validator. + * + * Nesting validators allows you to define validators for array + * types. For example, nested validators are ideal when you want to validate a + * sub-document, or complex array type. + * + * This method assumes that the sub-document has a 1:1 relationship with the parent. + * + * The providers of the parent validator will be synced into the nested validator, when + * errors are checked. This ensures that any validation rule providers connected + * in the parent will have the same values in the nested validator when rules are evaluated. + * + * @param string $field The root field for the nested validator. + * @param \Cake\Validation\Validator $validator The nested validator. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @return $this + */ + public function addNested($field, Validator $validator, $message = null, $when = null) + { + $extra = array_filter(['message' => $message, 'on' => $when]); + + $field = $this->field($field); + $field->add(static::NESTED, $extra + ['rule' => function ($value, $context) use ($validator, $message) { + if (!is_array($value)) { + return false; + } + foreach ($this->providers() as $provider) { + $validator->setProvider($provider, $this->getProvider($provider)); + } + $errors = $validator->errors($value, $context['newRecord']); + + $message = $message ? [static::NESTED => $message] : []; + + return empty($errors) ? true : $errors + $message; + }]); + + return $this; + } + + /** + * Adds a nested validator. + * + * Nesting validators allows you to define validators for array + * types. For example, nested validators are ideal when you want to validate many + * similar sub-documents or complex array types. + * + * This method assumes that the sub-document has a 1:N relationship with the parent. + * + * The providers of the parent validator will be synced into the nested validator, when + * errors are checked. This ensures that any validation rule providers connected + * in the parent will have the same values in the nested validator when rules are evaluated. + * + * @param string $field The root field for the nested validator. + * @param \Cake\Validation\Validator $validator The nested validator. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @return $this + */ + public function addNestedMany($field, Validator $validator, $message = null, $when = null) + { + $extra = array_filter(['message' => $message, 'on' => $when]); + + $field = $this->field($field); + $field->add(static::NESTED, $extra + ['rule' => function ($value, $context) use ($validator, $message) { + if (!is_array($value)) { + return false; + } + foreach ($this->providers() as $provider) { + $validator->setProvider($provider, $this->getProvider($provider)); + } + $errors = []; + foreach ($value as $i => $row) { + if (!is_array($row)) { + return false; + } + $check = $validator->errors($row, $context['newRecord']); + if (!empty($check)) { + $errors[$i] = $check; + } + } + + $message = $message ? [static::NESTED => $message] : []; + + return empty($errors) ? true : $errors + $message; + }]); + + return $this; + } + + /** + * Removes a rule from the set by its name + * + * ### Example: + * + * ``` + * $validator + * ->remove('title', 'required') + * ->remove('user_id') + * ``` + * + * @param string $field The name of the field from which the rule will be removed + * @param string|null $rule the name of the rule to be removed + * @return $this + */ + public function remove($field, $rule = null) + { + if ($rule === null) { + unset($this->_fields[$field]); + } else { + $this->field($field)->remove($rule); + } + + return $this; + } + + /** + * Sets whether a field is required to be present in data array. + * You can also pass array. Using an array will let you provide the following + * keys: + * + * - `mode` individual mode for field + * - `message` individual error message for field + * + * You can also set mode and message for all passed fields, the individual + * setting takes precedence over group settings. + * + * @param string|array $field the name of the field or list of fields. + * @param bool|string|callable $mode Valid values are true, false, 'create', 'update'. + * If a callable is passed then the field will be required only when the callback + * returns true. + * @param string|null $message The message to show if the field presence validation fails. + * @return $this + */ + public function requirePresence($field, $mode = true, $message = null) + { + $defaults = [ + 'mode' => $mode, + 'message' => $message + ]; + + if (!is_array($field)) { + $field = $this->_convertValidatorToArray($field, $defaults); + } + + foreach ($field as $fieldName => $setting) { + $settings = $this->_convertValidatorToArray($fieldName, $defaults, $setting); + $fieldName = current(array_keys($settings)); + + $this->field($fieldName)->requirePresence($settings[$fieldName]['mode']); + if ($settings[$fieldName]['message']) { + $this->_presenceMessages[$fieldName] = $settings[$fieldName]['message']; + } + } + + return $this; + } + + /** + * Allows a field to be empty. You can also pass array. + * Using an array will let you provide the following keys: + * + * - `when` individual when condition for field + * - 'message' individual message for field + * + * You can also set when and message for all passed fields, the individual setting + * takes precedence over group settings. + * + * This is the opposite of notEmpty() which requires a field to not be empty. + * By using $mode equal to 'create' or 'update', you can allow fields to be empty + * when records are first created, or when they are updated. + * + * ### Example: + * + * ``` + * // Email can be empty + * $validator->allowEmpty('email'); + * + * // Email can be empty on create + * $validator->allowEmpty('email', 'create'); + * + * // Email can be empty on update + * $validator->allowEmpty('email', 'update'); + * + * // Email and subject can be empty on update + * $validator->allowEmpty(['email', 'subject'], 'update'); + * + * // Email can be always empty, subject and content can be empty on update. + * $validator->allowEmpty( + * [ + * 'email' => [ + * 'when' => true + * ], + * 'content' => [ + * 'message' => 'Content cannot be empty' + * ], + * 'subject' + * ], + * 'update' + * ); + * ``` + * + * It is possible to conditionally allow emptiness on a field by passing a callback + * as a second argument. The callback will receive the validation context array as + * argument: + * + * ``` + * $validator->allowEmpty('email', function ($context) { + * return !$context['newRecord'] || $context['data']['role'] === 'admin'; + * }); + * ``` + * + * This method will correctly detect empty file uploads and date/time/datetime fields. + * + * Because this and `notEmpty()` modify the same internal state, the last + * method called will take precedence. + * + * @param string|array $field the name of the field or a list of fields + * @param bool|string|callable $when Indicates when the field is allowed to be empty + * Valid values are true (always), 'create', 'update'. If a callable is passed then + * the field will allowed to be empty only when the callback returns true. + * @param string|null $message The message to show if the field is not + * @return $this + */ + public function allowEmpty($field, $when = true, $message = null) + { + $settingsDefault = [ + 'when' => $when, + 'message' => $message + ]; + + if (!is_array($field)) { + $field = $this->_convertValidatorToArray($field, $settingsDefault); + } + + foreach ($field as $fieldName => $setting) { + $settings = $this->_convertValidatorToArray($fieldName, $settingsDefault, $setting); + $fieldName = current(array_keys($settings)); + + $this->field($fieldName)->allowEmpty($settings[$fieldName]['when']); + if ($settings[$fieldName]['message']) { + $this->_allowEmptyMessages[$fieldName] = $settings[$fieldName]['message']; + } + } + + return $this; + } + + /** + * Converts validator to fieldName => $settings array + * + * @param int|string $fieldName name of field + * @param array $defaults default settings + * @param string|array $settings settings from data + * @return array + */ + protected function _convertValidatorToArray($fieldName, $defaults = [], $settings = []) + { + if (is_string($settings)) { + $fieldName = $settings; + $settings = []; + } + if (!is_array($settings)) { + throw new InvalidArgumentException( + sprintf('Invalid settings for "%s". Settings must be an array.', $fieldName) + ); + } + $settings += $defaults; + + return [$fieldName => $settings]; + } + + /** + * Sets a field to require a non-empty value. You can also pass array. + * Using an array will let you provide the following keys: + * + * - `when` individual when condition for field + * - `message` individual error message for field + * + * You can also set `when` and `message` for all passed fields, the individual setting + * takes precedence over group settings. + * + * This is the opposite of `allowEmpty()` which allows a field to be empty. + * By using $mode equal to 'create' or 'update', you can make fields required + * when records are first created, or when they are updated. + * + * ### Example: + * + * ``` + * $message = 'This field cannot be empty'; + * + * // Email cannot be empty + * $validator->notEmpty('email'); + * + * // Email can be empty on update, but not create + * $validator->notEmpty('email', $message, 'create'); + * + * // Email can be empty on create, but required on update. + * $validator->notEmpty('email', $message, 'update'); + * + * // Email and title can be empty on create, but are required on update. + * $validator->notEmpty(['email', 'title'], $message, 'update'); + * + * // Email can be empty on create, title must always be not empty + * $validator->notEmpty( + * [ + * 'email', + * 'title' => [ + * 'when' => true, + * 'message' => 'Title cannot be empty' + * ] + * ], + * $message, + * 'update' + * ); + * ``` + * + * It is possible to conditionally disallow emptiness on a field by passing a callback + * as the third argument. The callback will receive the validation context array as + * argument: + * + * ``` + * $validator->notEmpty('email', 'Email is required', function ($context) { + * return $context['newRecord'] && $context['data']['role'] !== 'admin'; + * }); + * ``` + * + * Because this and `allowEmpty()` modify the same internal state, the last + * method called will take precedence. + * + * @param string|array $field the name of the field or list of fields + * @param string|null $message The message to show if the field is not + * @param bool|string|callable $when Indicates when the field is not allowed + * to be empty. Valid values are true (always), 'create', 'update'. If a + * callable is passed then the field will allowed to be empty only when + * the callback returns false. + * @return $this + */ + public function notEmpty($field, $message = null, $when = false) + { + $defaults = [ + 'when' => $when, + 'message' => $message + ]; + + if (!is_array($field)) { + $field = $this->_convertValidatorToArray($field, $defaults); + } + + foreach ($field as $fieldName => $setting) { + $settings = $this->_convertValidatorToArray($fieldName, $defaults, $setting); + $fieldName = current(array_keys($settings)); + $whenSetting = $settings[$fieldName]['when']; + + if ($whenSetting === 'create' || $whenSetting === 'update') { + $whenSetting = $whenSetting === 'create' ? 'update' : 'create'; + } elseif (is_callable($whenSetting)) { + $whenSetting = function ($context) use ($whenSetting) { + return !$whenSetting($context); + }; + } + + $this->field($fieldName)->allowEmpty($whenSetting); + if ($settings[$fieldName]['message']) { + $this->_allowEmptyMessages[$fieldName] = $settings[$fieldName]['message']; + } + } + + return $this; + } + + /** + * Add a notBlank rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::notBlank() + * @return $this + */ + public function notBlank($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'notBlank', $extra + [ + 'rule' => 'notBlank', + ]); + } + + /** + * Add an alphanumeric rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::alphaNumeric() + * @return $this + */ + public function alphaNumeric($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'alphaNumeric', $extra + [ + 'rule' => 'alphaNumeric', + ]); + } + + /** + * Add an rule that ensures a string length is within a range. + * + * @param string $field The field you want to apply the rule to. + * @param array $range The inclusive minimum and maximum length you want permitted. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::alphaNumeric() + * @return $this + */ + public function lengthBetween($field, array $range, $message = null, $when = null) + { + if (count($range) !== 2) { + throw new InvalidArgumentException('The $range argument requires 2 numbers'); + } + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'lengthBetween', $extra + [ + 'rule' => ['lengthBetween', array_shift($range), array_shift($range)], + ]); + } + + /** + * Add a credit card rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string $type The type of cards you want to allow. Defaults to 'all'. + * You can also supply an array of accepted card types. e.g `['mastercard', 'visa', 'amex']` + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::cc() + * @return $this + */ + public function creditCard($field, $type = 'all', $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'creditCard', $extra + [ + 'rule' => ['cc', $type, true], + ]); + } + + /** + * Add a greater than comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be greater than. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::comparison() + * @return $this + */ + public function greaterThan($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'greaterThan', $extra + [ + 'rule' => ['comparison', Validation::COMPARE_GREATER, $value] + ]); + } + + /** + * Add a greater than or equal to comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be greater than or equal to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::comparison() + * @return $this + */ + public function greaterThanOrEqual($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'greaterThanOrEqual', $extra + [ + 'rule' => ['comparison', Validation::COMPARE_GREATER_OR_EQUAL, $value] + ]); + } + + /** + * Add a less than comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be less than. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::comparison() + * @return $this + */ + public function lessThan($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'lessThan', $extra + [ + 'rule' => ['comparison', Validation::COMPARE_LESS, $value] + ]); + } + + /** + * Add a less than or equal comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be less than or equal to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::comparison() + * @return $this + */ + public function lessThanOrEqual($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'lessThanOrEqual', $extra + [ + 'rule' => ['comparison', Validation::COMPARE_LESS_OR_EQUAL, $value] + ]); + } + + /** + * Add a equal to comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be equal to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::comparison() + * @return $this + */ + public function equals($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'equals', $extra + [ + 'rule' => ['comparison', Validation::COMPARE_EQUAL, $value] + ]); + } + + /** + * Add a not equal to comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be not be equal to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::comparison() + * @return $this + */ + public function notEquals($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'notEquals', $extra + [ + 'rule' => ['comparison', Validation::COMPARE_NOT_EQUAL, $value] + ]); + } + + /** + * Add a rule to compare two fields to each other. + * + * If both fields have the exact same value the rule will pass. + * + * @param string $field The field you want to apply the rule to. + * @param string $secondField The field you want to compare against. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::compareFields() + * @return $this + */ + public function sameAs($field, $secondField, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'sameAs', $extra + [ + 'rule' => ['compareFields', $secondField, Validation::COMPARE_SAME] + ]); + } + + /** + * Add a rule to compare that two fields have different values. + * + * @param string $field The field you want to apply the rule to. + * @param string $secondField The field you want to compare against. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::compareFields() + * @return $this + * @since 3.6.0 + */ + public function notSameAs($field, $secondField, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'notSameAs', $extra + [ + 'rule' => ['compareFields', $secondField, Validation::COMPARE_NOT_SAME] + ]); + } + + /** + * Add a rule to compare one field is equal to another. + * + * @param string $field The field you want to apply the rule to. + * @param string $secondField The field you want to compare against. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::compareFields() + * @return $this + * @since 3.6.0 + */ + public function equalToField($field, $secondField, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'equalToField', $extra + [ + 'rule' => ['compareFields', $secondField, Validation::COMPARE_EQUAL] + ]); + } + + /** + * Add a rule to compare one field is not equal to another. + * + * @param string $field The field you want to apply the rule to. + * @param string $secondField The field you want to compare against. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::compareFields() + * @return $this + * @since 3.6.0 + */ + public function notEqualToField($field, $secondField, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'notEqualToField', $extra + [ + 'rule' => ['compareFields', $secondField, Validation::COMPARE_NOT_EQUAL] + ]); + } + + /** + * Add a rule to compare one field is greater than another. + * + * @param string $field The field you want to apply the rule to. + * @param string $secondField The field you want to compare against. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::compareFields() + * @return $this + * @since 3.6.0 + */ + public function greaterThanField($field, $secondField, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'greaterThanField', $extra + [ + 'rule' => ['compareFields', $secondField, Validation::COMPARE_GREATER] + ]); + } + + /** + * Add a rule to compare one field is greater than or equal to another. + * + * @param string $field The field you want to apply the rule to. + * @param string $secondField The field you want to compare against. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::compareFields() + * @return $this + * @since 3.6.0 + */ + public function greaterThanOrEqualToField($field, $secondField, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'greaterThanOrEqualToField', $extra + [ + 'rule' => ['compareFields', $secondField, Validation::COMPARE_GREATER_OR_EQUAL] + ]); + } + + /** + * Add a rule to compare one field is less than another. + * + * @param string $field The field you want to apply the rule to. + * @param string $secondField The field you want to compare against. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::compareFields() + * @return $this + * @since 3.6.0 + */ + public function lessThanField($field, $secondField, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'lessThanField', $extra + [ + 'rule' => ['compareFields', $secondField, Validation::COMPARE_LESS] + ]); + } + + /** + * Add a rule to compare one field is less than or equal to another. + * + * @param string $field The field you want to apply the rule to. + * @param string $secondField The field you want to compare against. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::compareFields() + * @return $this + * @since 3.6.0 + */ + public function lessThanOrEqualToField($field, $secondField, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'lessThanOrEqualToField', $extra + [ + 'rule' => ['compareFields', $secondField, Validation::COMPARE_LESS_OR_EQUAL] + ]); + } + + /** + * Add a rule to check if a field contains non alpha numeric characters. + * + * @param string $field The field you want to apply the rule to. + * @param int $limit The minimum number of non-alphanumeric fields required. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::containsNonAlphaNumeric() + * @return $this + */ + public function containsNonAlphaNumeric($field, $limit = 1, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'containsNonAlphaNumeric', $extra + [ + 'rule' => ['containsNonAlphaNumeric', $limit] + ]); + } + + /** + * Add a date format validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param array $formats A list of accepted date formats. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::date() + * @return $this + */ + public function date($field, $formats = ['ymd'], $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'date', $extra + [ + 'rule' => ['date', $formats] + ]); + } + + /** + * Add a date time format validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param array $formats A list of accepted date formats. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::datetime() + * @return $this + */ + public function dateTime($field, $formats = ['ymd'], $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'dateTime', $extra + [ + 'rule' => ['datetime', $formats] + ]); + } + + /** + * Add a time format validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::time() + * @return $this + */ + public function time($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'time', $extra + [ + 'rule' => 'time' + ]); + } + + /** + * Add a localized time, date or datetime format validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string $type Parser type, one out of 'date', 'time', and 'datetime' + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::localizedTime() + * @return $this + */ + public function localizedTime($field, $type = 'datetime', $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'localizedTime', $extra + [ + 'rule' => ['localizedTime', $type] + ]); + } + + /** + * Add a boolean validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::boolean() + * @return $this + */ + public function boolean($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'boolean', $extra + [ + 'rule' => 'boolean' + ]); + } + + /** + * Add a decimal validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|null $places The number of decimal places to require. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::decimal() + * @return $this + */ + public function decimal($field, $places = null, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'decimal', $extra + [ + 'rule' => ['decimal', $places] + ]); + } + + /** + * Add an email validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param bool $checkMX Whether or not to check the MX records. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::email() + * @return $this + */ + public function email($field, $checkMX = false, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'email', $extra + [ + 'rule' => ['email', $checkMX] + ]); + } + + /** + * Add an IP validation rule to a field. + * + * This rule will accept both IPv4 and IPv6 addresses. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::ip() + * @return $this + */ + public function ip($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'ip', $extra + [ + 'rule' => 'ip' + ]); + } + + /** + * Add an IPv4 validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::ip() + * @return $this + */ + public function ipv4($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'ipv4', $extra + [ + 'rule' => ['ip', 'ipv4'] + ]); + } + + /** + * Add an IPv6 validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::ip() + * @return $this + */ + public function ipv6($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'ipv6', $extra + [ + 'rule' => ['ip', 'ipv6'] + ]); + } + + /** + * Add a string length validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int $min The minimum length required. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::minLength() + * @return $this + */ + public function minLength($field, $min, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'minLength', $extra + [ + 'rule' => ['minLength', $min] + ]); + } + + /** + * Add a string length validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int $min The minimum length required. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::minLengthBytes() + * @return $this + */ + public function minLengthBytes($field, $min, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'minLengthBytes', $extra + [ + 'rule' => ['minLengthBytes', $min] + ]); + } + + /** + * Add a string length validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int $max The maximum length allowed. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::maxLength() + * @return $this + */ + public function maxLength($field, $max, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'maxLength', $extra + [ + 'rule' => ['maxLength', $max] + ]); + } + + /** + * Add a string length validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int $max The maximum length allowed. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::maxLengthBytes() + * @return $this + */ + public function maxLengthBytes($field, $max, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'maxLengthBytes', $extra + [ + 'rule' => ['maxLengthBytes', $max] + ]); + } + + /** + * Add a numeric value validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::numeric() + * @return $this + */ + public function numeric($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'numeric', $extra + [ + 'rule' => 'numeric' + ]); + } + + /** + * Add a natural number validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::naturalNumber() + * @return $this + */ + public function naturalNumber($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'naturalNumber', $extra + [ + 'rule' => ['naturalNumber', false] + ]); + } + + /** + * Add a validation rule to ensure a field is a non negative integer. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::naturalNumber() + * @return $this + */ + public function nonNegativeInteger($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'nonNegativeInteger', $extra + [ + 'rule' => ['naturalNumber', true] + ]); + } + + /** + * Add a validation rule to ensure a field is within a numeric range + * + * @param string $field The field you want to apply the rule to. + * @param array $range The inclusive upper and lower bounds of the valid range. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::range() + * @return $this + */ + public function range($field, array $range, $message = null, $when = null) + { + if (count($range) !== 2) { + throw new InvalidArgumentException('The $range argument requires 2 numbers'); + } + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'range', $extra + [ + 'rule' => ['range', array_shift($range), array_shift($range)] + ]); + } + + /** + * Add a validation rule to ensure a field is a URL. + * + * This validator does not require a protocol. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::url() + * @return $this + */ + public function url($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'url', $extra + [ + 'rule' => ['url', false] + ]); + } + + /** + * Add a validation rule to ensure a field is a URL. + * + * This validator requires the URL to have a protocol. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::url() + * @return $this + */ + public function urlWithProtocol($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'urlWithProtocol', $extra + [ + 'rule' => ['url', true] + ]); + } + + /** + * Add a validation rule to ensure the field value is within a whitelist. + * + * @param string $field The field you want to apply the rule to. + * @param array $list The list of valid options. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::inList() + * @return $this + */ + public function inList($field, array $list, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'inList', $extra + [ + 'rule' => ['inList', $list] + ]); + } + + /** + * Add a validation rule to ensure the field is a UUID + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::uuid() + * @return $this + */ + public function uuid($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'uuid', $extra + [ + 'rule' => 'uuid' + ]); + } + + /** + * Add a validation rule to ensure the field is an uploaded file + * + * For options see Cake\Validation\Validation::uploadedFile() + * + * @param string $field The field you want to apply the rule to. + * @param array $options An array of options. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::uploadedFile() + * @return $this + */ + public function uploadedFile($field, array $options, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'uploadedFile', $extra + [ + 'rule' => ['uploadedFile', $options] + ]); + } + + /** + * Add a validation rule to ensure the field is a lat/long tuple. + * + * e.g. `, ` + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::uuid() + * @return $this + */ + public function latLong($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'latLong', $extra + [ + 'rule' => 'geoCoordinate' + ]); + } + + /** + * Add a validation rule to ensure the field is a latitude. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::latitude() + * @return $this + */ + public function latitude($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'latitude', $extra + [ + 'rule' => 'latitude' + ]); + } + + /** + * Add a validation rule to ensure the field is a longitude. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::longitude() + * @return $this + */ + public function longitude($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'longitude', $extra + [ + 'rule' => 'longitude' + ]); + } + + /** + * Add a validation rule to ensure a field contains only ascii bytes + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::ascii() + * @return $this + */ + public function ascii($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'ascii', $extra + [ + 'rule' => 'ascii' + ]); + } + + /** + * Add a validation rule to ensure a field contains only BMP utf8 bytes + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::utf8() + * @return $this + */ + public function utf8($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'utf8', $extra + [ + 'rule' => ['utf8', ['extended' => false]] + ]); + } + + /** + * Add a validation rule to ensure a field contains only utf8 bytes. + * + * This rule will accept 3 and 4 byte UTF8 sequences, which are necessary for emoji. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::utf8() + * @return $this + */ + public function utf8Extended($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'utf8Extended', $extra + [ + 'rule' => ['utf8', ['extended' => true]] + ]); + } + + /** + * Add a validation rule to ensure a field is an integer value. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::isInteger() + * @return $this + */ + public function integer($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'integer', $extra + [ + 'rule' => 'isInteger' + ]); + } + + /** + * Add a validation rule to ensure that a field contains an array. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::isArray() + * @return $this + */ + public function isArray($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'isArray', $extra + [ + 'rule' => 'isArray' + ]); + } + + /** + * Add a validation rule to ensure that a field contains a scalar. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::isScalar() + * @return $this + */ + public function scalar($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'scalar', $extra + [ + 'rule' => 'isScalar' + ]); + } + + /** + * Add a validation rule to ensure a field is a 6 digits hex color value. + * + * @param string $field The field you want to apply the rule to. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::hexColor() + * @return $this + */ + public function hexColor($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'hexColor', $extra + [ + 'rule' => 'hexColor', + ]); + } + + /** + * Add a validation rule for a multiple select. Comparison is case sensitive by default. + * + * @param string $field The field you want to apply the rule to. + * @param array $options The options for the validator. Includes the options defined in + * \Cake\Validation\Validation::multiple() and the `caseInsensitive` parameter. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::multiple() + * @return $this + */ + public function multipleOptions($field, array $options = [], $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + $caseInsensitive = isset($options['caseInsensitive']) ? $options['caseInsensitive'] : false; + unset($options['caseInsensitive']); + + return $this->add($field, 'multipleOptions', $extra + [ + 'rule' => ['multiple', $options, $caseInsensitive] + ]); + } + + /** + * Add a validation rule to ensure that a field is an array containing at least + * the specified amount of elements + * + * @param string $field The field you want to apply the rule to. + * @param int $count The number of elements the array should at least have + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::numElements() + * @return $this + */ + public function hasAtLeast($field, $count, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'hasAtLeast', $extra + [ + 'rule' => function ($value) use ($count) { + if (is_array($value) && isset($value['_ids'])) { + $value = $value['_ids']; + } + + return Validation::numElements($value, Validation::COMPARE_GREATER_OR_EQUAL, $count); + } + ]); + } + + /** + * Add a validation rule to ensure that a field is an array containing at most + * the specified amount of elements + * + * @param string $field The field you want to apply the rule to. + * @param int $count The number maximum amount of elements the field should have + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @see \Cake\Validation\Validation::numElements() + * @return $this + */ + public function hasAtMost($field, $count, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'hasAtMost', $extra + [ + 'rule' => function ($value) use ($count) { + if (is_array($value) && isset($value['_ids'])) { + $value = $value['_ids']; + } + + return Validation::numElements($value, Validation::COMPARE_LESS_OR_EQUAL, $count); + } + ]); + } + + /** + * Returns whether or not a field can be left empty for a new or already existing + * record. + * + * @param string $field Field name. + * @param bool $newRecord whether the data to be validated is new or to be updated. + * @return bool + */ + public function isEmptyAllowed($field, $newRecord) + { + $providers = $this->_providers; + $data = []; + $context = compact('data', 'newRecord', 'field', 'providers'); + + return $this->_canBeEmpty($this->field($field), $context); + } + + /** + * Returns whether or not a field can be left out for a new or already existing + * record. + * + * @param string $field Field name. + * @param bool $newRecord Whether the data to be validated is new or to be updated. + * @return bool + */ + public function isPresenceRequired($field, $newRecord) + { + $providers = $this->_providers; + $data = []; + $context = compact('data', 'newRecord', 'field', 'providers'); + + return !$this->_checkPresence($this->field($field), $context); + } + + /** + * Returns whether or not a field matches against a regular expression. + * + * @param string $field Field name. + * @param string $regex Regular expression. + * @param string|null $message The error message when the rule fails. + * @param string|callable|null $when Either 'create' or 'update' or a callable that returns + * true when the validation rule should be applied. + * @return $this + */ + public function regex($field, $regex, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + + return $this->add($field, 'regex', $extra + [ + 'rule' => ['custom', $regex] + ]); + } + + /** + * Returns false if any validation for the passed rule set should be stopped + * due to the field missing in the data array + * + * @param \Cake\Validation\ValidationSet $field The set of rules for a field. + * @param array $context A key value list of data containing the validation context. + * @return bool + */ + protected function _checkPresence($field, $context) + { + $required = $field->isPresenceRequired(); + + if (!is_string($required) && is_callable($required)) { + return !$required($context); + } + + $newRecord = $context['newRecord']; + if (in_array($required, ['create', 'update'], true)) { + return ( + ($required === 'create' && !$newRecord) || + ($required === 'update' && $newRecord) + ); + } + + return !$required; + } + + /** + * Returns whether the field can be left blank according to `allowEmpty` + * + * @param \Cake\Validation\ValidationSet $field the set of rules for a field + * @param array $context a key value list of data containing the validation context. + * @return bool + */ + protected function _canBeEmpty($field, $context) + { + $allowed = $field->isEmptyAllowed(); + + if (!is_string($allowed) && is_callable($allowed)) { + return $allowed($context); + } + + $newRecord = $context['newRecord']; + if (in_array($allowed, ['create', 'update'], true)) { + $allowed = ( + ($allowed === 'create' && $newRecord) || + ($allowed === 'update' && !$newRecord) + ); + } + + return $allowed; + } + + /** + * Returns true if the field is empty in the passed data array + * + * @param mixed $data value to check against + * @return bool + */ + protected function _fieldIsEmpty($data) + { + if (empty($data) && !is_bool($data) && !is_numeric($data)) { + return true; + } + $isArray = is_array($data); + if ($isArray && (isset($data['year']) || isset($data['hour']))) { + $value = implode('', $data); + + return strlen($value) === 0; + } + if ($isArray && isset($data['name'], $data['type'], $data['tmp_name'], $data['error'])) { + return (int)$data['error'] === UPLOAD_ERR_NO_FILE; + } + + return false; + } + + /** + * Iterates over each rule in the validation set and collects the errors resulting + * from executing them + * + * @param string $field The name of the field that is being processed + * @param \Cake\Validation\ValidationSet $rules the list of rules for a field + * @param array $data the full data passed to the validator + * @param bool $newRecord whether is it a new record or an existing one + * @return array + */ + protected function _processRules($field, ValidationSet $rules, $data, $newRecord) + { + $errors = []; + // Loading default provider in case there is none + $this->getProvider('default'); + $message = 'The provided value is invalid'; + + if ($this->_useI18n) { + $message = __d('cake', 'The provided value is invalid'); + } + + foreach ($rules as $name => $rule) { + $result = $rule->process($data[$field], $this->_providers, compact('newRecord', 'data', 'field')); + if ($result === true) { + continue; + } + + $errors[$name] = $message; + if (is_array($result) && $name === static::NESTED) { + $errors = $result; + } + if (is_string($result)) { + $errors[$name] = $result; + } + + if ($rule->isLast()) { + break; + } + } + + return $errors; + } + + /** + * Get the printable version of this object. + * + * @return array + */ + public function __debugInfo() + { + $fields = []; + foreach ($this->_fields as $name => $fieldSet) { + $fields[$name] = [ + 'isPresenceRequired' => $fieldSet->isPresenceRequired(), + 'isEmptyAllowed' => $fieldSet->isEmptyAllowed(), + 'rules' => array_keys($fieldSet->rules()), + ]; + } + + return [ + '_presenceMessages' => $this->_presenceMessages, + '_allowEmptyMessages' => $this->_allowEmptyMessages, + '_useI18n' => $this->_useI18n, + '_providers' => array_keys($this->_providers), + '_fields' => $fields + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Validation/ValidatorAwareInterface.php b/app/vendor/cakephp/cakephp/src/Validation/ValidatorAwareInterface.php new file mode 100644 index 000000000..e7f67b2a0 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Validation/ValidatorAwareInterface.php @@ -0,0 +1,58 @@ +add('email', 'valid-email', ['rule' => 'email']) + * ->add('password', 'valid', ['rule' => 'notBlank']) + * ->requirePresence('username'); + * } + * ``` + * + * Otherwise, you can build the object by yourself and store it in the Table object: + * + * ``` + * $validator = new \Cake\Validation\Validator($table); + * $validator + * ->add('email', 'valid-email', ['rule' => 'email']) + * ->add('password', 'valid', ['rule' => 'notBlank']) + * ->allowEmpty('bio'); + * $table->setValidator('forSubscription', $validator); + * ``` + * + * You can implement the method in `validationDefault` in your Table subclass + * should you wish to have a validation set that applies in cases where no other + * set is specified. + * + * @param string|null $name the name of the validation set to return + * @param \Cake\Validation\Validator|null $validator The validator instance to store, + * use null to get a validator. + * @return \Cake\Validation\Validator + * @throws \RuntimeException + * @deprecated 3.5.0 Use getValidator/setValidator instead. + */ + public function validator($name = null, Validator $validator = null) + { + deprecationWarning( + 'ValidatorAwareTrait::validator() is deprecated. ' . + 'Use ValidatorAwareTrait::getValidator()/setValidator() instead.' + ); + if ($validator !== null) { + $name = $name ?: self::DEFAULT_VALIDATOR; + $this->setValidator($name, $validator); + } + + return $this->getValidator($name); + } + + /** + * Returns the validation rules tagged with $name. It is possible to have + * multiple different named validation sets, this is useful when you need + * to use varying rules when saving from different routines in your system. + * + * If a validator has not been set earlier, this method will build a valiator + * using a method inside your class. + * + * For example, if you wish to create a validation set called 'forSubscription', + * you will need to create a method in your Table subclass as follows: + * + * ``` + * public function validationForSubscription($validator) + * { + * return $validator + * ->add('email', 'valid-email', ['rule' => 'email']) + * ->add('password', 'valid', ['rule' => 'notBlank']) + * ->requirePresence('username'); + * } + * $validator = $this->getValidator('forSubscription'); + * ``` + * + * You can implement the method in `validationDefault` in your Table subclass + * should you wish to have a validation set that applies in cases where no other + * set is specified. + * + * If a $name argument has not been provided, the default validator will be returned. + * You can configure your default validator name in a `DEFAULT_VALIDATOR` + * class constant. + * + * @param string|null $name The name of the validation set to return. + * @return \Cake\Validation\Validator + */ + public function getValidator($name = null) + { + $name = $name ?: self::DEFAULT_VALIDATOR; + if (!isset($this->_validators[$name])) { + $validator = $this->createValidator($name); + $this->setValidator($name, $validator); + } + + return $this->_validators[$name]; + } + + /** + * Creates a validator using a custom method inside your class. + * + * This method is used only to build a new validator and it does not store + * it in your object. If you want to build and reuse validators, + * use getValidator() method instead. + * + * @param string $name The name of the validation set to create. + * @return \Cake\Validation\Validator + * @throws \RuntimeException + */ + protected function createValidator($name) + { + $method = 'validation' . ucfirst($name); + if (!$this->validationMethodExists($method)) { + $message = sprintf('The %s::%s() validation method does not exists.', __CLASS__, $method); + throw new RuntimeException($message); + } + + $validator = new $this->_validatorClass; + $validator = $this->$method($validator); + if ($this instanceof EventDispatcherInterface) { + $event = defined(self::class . '::BUILD_VALIDATOR_EVENT') ? self::BUILD_VALIDATOR_EVENT : 'Model.buildValidator'; + $this->dispatchEvent($event, compact('validator', 'name')); + } + + if (!$validator instanceof Validator) { + throw new RuntimeException(sprintf('The %s::%s() validation method must return an instance of %s.', __CLASS__, $method, Validator::class)); + } + + return $validator; + } + + /** + * This method stores a custom validator under the given name. + * + * You can build the object by yourself and store it in your object: + * + * ``` + * $validator = new \Cake\Validation\Validator($table); + * $validator + * ->add('email', 'valid-email', ['rule' => 'email']) + * ->add('password', 'valid', ['rule' => 'notBlank']) + * ->allowEmpty('bio'); + * $this->setValidator('forSubscription', $validator); + * ``` + * + * @param string $name The name of a validator to be set. + * @param \Cake\Validation\Validator $validator Validator object to be set. + * @return $this + */ + public function setValidator($name, Validator $validator) + { + $validator->setProvider(self::VALIDATOR_PROVIDER_NAME, $this); + $this->_validators[$name] = $validator; + + return $this; + } + + /** + * Checks whether or not a validator has been set. + * + * @param string $name The name of a validator. + * @return bool + */ + public function hasValidator($name) + { + $method = 'validation' . ucfirst($name); + if ($this->validationMethodExists($method)) { + return true; + } + + return isset($this->_validators[$name]); + } + + /** + * Checks if validation method exists. + * + * @param string $name Validation method name. + * @return bool + */ + protected function validationMethodExists($name) + { + return method_exists($this, $name); + } + + /** + * Returns the default validator object. Subclasses can override this function + * to add a default validation set to the validator object. + * + * @param \Cake\Validation\Validator $validator The validator that can be modified to + * add some rules to it. + * @return \Cake\Validation\Validator + */ + public function validationDefault(Validator $validator) + { + return $validator; + } +} diff --git a/app/vendor/cakephp/cakephp/src/Validation/composer.json b/app/vendor/cakephp/cakephp/src/Validation/composer.json new file mode 100644 index 000000000..e4de56aa7 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/Validation/composer.json @@ -0,0 +1,38 @@ +{ + "name": "cakephp/validation", + "description": "CakePHP Validation library", + "type": "library", + "keywords": [ + "cakephp", + "validation", + "data validation" + ], + "homepage": "https://cakephp.org", + "license": "MIT", + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/validation/graphs/contributors" + } + ], + "support": { + "issues": "https://github.com/cakephp/cakephp/issues", + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "source": "https://github.com/cakephp/validation" + }, + "require": { + "php": ">=5.6.0", + "cakephp/core": "^3.6.0", + "cakephp/utility": "^3.6.0", + "psr/http-message": "^1.0.0" + }, + "suggest": { + "cakephp/i18n": "If you want to use Validation::localizedTime()" + }, + "autoload": { + "psr-4": { + "Cake\\Validation\\": "." + } + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/AjaxView.php b/app/vendor/cakephp/cakephp/src/View/AjaxView.php new file mode 100644 index 000000000..b63412ad8 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/AjaxView.php @@ -0,0 +1,55 @@ +withType('ajax'); + } + + parent::__construct($request, $response, $eventManager, $viewOptions); + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/Cell.php b/app/vendor/cakephp/cakephp/src/View/Cell.php new file mode 100644 index 000000000..87af43595 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/Cell.php @@ -0,0 +1,311 @@ +setEventManager($eventManager); + } + $this->request = $request; + $this->response = $response; + $this->modelFactory('Table', [$this->getTableLocator(), 'get']); + + $this->_validCellOptions = array_merge(['action', 'args'], $this->_validCellOptions); + foreach ($this->_validCellOptions as $var) { + if (isset($cellOptions[$var])) { + $this->{$var} = $cellOptions[$var]; + } + } + if (!empty($cellOptions['cache'])) { + $this->_cache = $cellOptions['cache']; + } + + $this->initialize(); + } + + /** + * Initialization hook method. + * + * Implement this method to avoid having to overwrite + * the constructor and calling parent::__construct(). + * + * @return void + */ + public function initialize() + { + } + + /** + * Render the cell. + * + * @param string|null $template Custom template name to render. If not provided (null), the last + * value will be used. This value is automatically set by `CellTrait::cell()`. + * @return string The rendered cell. + * @throws \Cake\View\Exception\MissingCellViewException When a MissingTemplateException is raised during rendering. + */ + public function render($template = null) + { + $cache = []; + if ($this->_cache) { + $cache = $this->_cacheConfig($this->action, $template); + } + + $render = function () use ($template) { + try { + $reflect = new ReflectionMethod($this, $this->action); + $reflect->invokeArgs($this, $this->args); + } catch (ReflectionException $e) { + throw new BadMethodCallException(sprintf( + 'Class %s does not have a "%s" method.', + get_class($this), + $this->action + )); + } + + $builder = $this->viewBuilder(); + + if ($template !== null && + strpos($template, '/') === false && + strpos($template, '.') === false + ) { + $template = Inflector::underscore($template); + } + if ($template === null) { + $template = $builder->getTemplate() ?: $this->template; + } + $builder->setLayout(false) + ->setTemplate($template); + + $className = get_class($this); + $namePrefix = '\View\Cell\\'; + $name = substr($className, strpos($className, $namePrefix) + strlen($namePrefix)); + $name = substr($name, 0, -4); + if (!$builder->getTemplatePath()) { + $builder->setTemplatePath('Cell' . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $name)); + } + + $this->View = $this->createView(); + try { + return $this->View->render($template); + } catch (MissingTemplateException $e) { + throw new MissingCellViewException(['file' => $template, 'name' => $name], null, $e); + } + }; + + if ($cache) { + return Cache::remember($cache['key'], $render, $cache['config']); + } + + return $render(); + } + + /** + * Generate the cache key to use for this cell. + * + * If the key is undefined, the cell class and action name will be used. + * + * @param string $action The action invoked. + * @param string|null $template The name of the template to be rendered. + * @return array The cache configuration. + */ + protected function _cacheConfig($action, $template = null) + { + if (empty($this->_cache)) { + return []; + } + $template = $template ?: 'default'; + $key = 'cell_' . Inflector::underscore(get_class($this)) . '_' . $action . '_' . $template; + $key = str_replace('\\', '_', $key); + $default = [ + 'config' => 'default', + 'key' => $key + ]; + if ($this->_cache === true) { + return $default; + } + + return $this->_cache + $default; + } + + /** + * Magic method. + * + * Starts the rendering process when Cell is echoed. + * + * *Note* This method will trigger an error when view rendering has a problem. + * This is because PHP will not allow a __toString() method to throw an exception. + * + * @return string Rendered cell + * @throws \Error Include error details for PHP 7 fatal errors. + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + trigger_error(sprintf('Could not render cell - %s [%s, line %d]', $e->getMessage(), $e->getFile(), $e->getLine()), E_USER_WARNING); + + return ''; + } catch (Error $e) { + throw new Error(sprintf('Could not render cell - %s [%s, line %d]', $e->getMessage(), $e->getFile(), $e->getLine())); + } + } + + /** + * Debug info. + * + * @return array + */ + public function __debugInfo() + { + return [ + 'plugin' => $this->plugin, + 'action' => $this->action, + 'args' => $this->args, + 'template' => $this->template, + 'request' => $this->request, + 'response' => $this->response, + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/CellTrait.php b/app/vendor/cakephp/cakephp/src/View/CellTrait.php new file mode 100644 index 000000000..cee1eff44 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/CellTrait.php @@ -0,0 +1,131 @@ +cell('Taxonomy.TagCloud::smallList', ['limit' => 10]); + * + * // App\View\Cell\TagCloudCell::smallList() + * $cell = $this->cell('TagCloud::smallList', ['limit' => 10]); + * ``` + * + * The `display` action will be used by default when no action is provided: + * + * ``` + * // Taxonomy\View\Cell\TagCloudCell::display() + * $cell = $this->cell('Taxonomy.TagCloud'); + * ``` + * + * Cells are not rendered until they are echoed. + * + * @param string $cell You must indicate cell name, and optionally a cell action. e.g.: `TagCloud::smallList` + * will invoke `View\Cell\TagCloudCell::smallList()`, `display` action will be invoked by default when none is provided. + * @param array $data Additional arguments for cell method. e.g.: + * `cell('TagCloud::smallList', ['a1' => 'v1', 'a2' => 'v2'])` maps to `View\Cell\TagCloud::smallList(v1, v2)` + * @param array $options Options for Cell's constructor + * @return \Cake\View\Cell The cell instance + * @throws \Cake\View\Exception\MissingCellException If Cell class was not found. + * @throws \BadMethodCallException If Cell class does not specified cell action. + */ + protected function cell($cell, array $data = [], array $options = []) + { + $parts = explode('::', $cell); + + if (count($parts) === 2) { + list($pluginAndCell, $action) = [$parts[0], $parts[1]]; + } else { + list($pluginAndCell, $action) = [$parts[0], 'display']; + } + + list($plugin) = pluginSplit($pluginAndCell); + $className = App::className($pluginAndCell, 'View/Cell', 'Cell'); + + if (!$className) { + throw new MissingCellException(['className' => $pluginAndCell . 'Cell']); + } + + if (!empty($data)) { + $data = array_values($data); + } + $options = ['action' => $action, 'args' => $data] + $options; + $cell = $this->_createCell($className, $action, $plugin, $options); + + return $cell; + } + + /** + * Create and configure the cell instance. + * + * @param string $className The cell classname. + * @param string $action The action name. + * @param string $plugin The plugin name. + * @param array $options The constructor options for the cell. + * @return \Cake\View\Cell + */ + protected function _createCell($className, $action, $plugin, $options) + { + /* @var \Cake\View\Cell $instance */ + $instance = new $className($this->request, $this->response, $this->getEventManager(), $options); + $instance->template = Inflector::underscore($action); + + $builder = $instance->viewBuilder(); + if (!empty($plugin)) { + $builder->setPlugin($plugin); + } + if (!empty($this->helpers)) { + $builder->setHelpers($this->helpers); + $instance->helpers = $this->helpers; + } + + if ($this instanceof View) { + if (!empty($this->theme)) { + $builder->setTheme($this->theme); + } + + $class = get_class($this); + $builder->setClassName($class); + $instance->viewClass = $class; + + return $instance; + } + + if (method_exists($this, 'viewBuilder')) { + $builder->setTheme($this->viewBuilder()->getTheme()); + } + + if (isset($this->viewClass)) { + $builder->setClassName($this->viewClass); + $instance->viewClass = $this->viewClass; + } + + return $instance; + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/Exception/MissingCellException.php b/app/vendor/cakephp/cakephp/src/View/Exception/MissingCellException.php new file mode 100644 index 000000000..1c2a4a293 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/Exception/MissingCellException.php @@ -0,0 +1,26 @@ + [ + * 'id' => ['type' => 'integer'], + * 'title' => ['type' => 'string', 'length' => 255], + * '_constraints' => [ + * 'primary' => ['type' => 'primary', 'columns' => ['id']] + * ] + * ], + * 'defaults' => [ + * 'id' => 1, + * 'title' => 'First post!', + * ] + * ]; + * ``` + */ +class ArrayContext implements ContextInterface +{ + + /** + * The request object. + * + * @var \Cake\Http\ServerRequest + */ + protected $_request; + + /** + * Context data for this object. + * + * @var array + */ + protected $_context; + + /** + * Constructor. + * + * @param \Cake\Http\ServerRequest $request The request object. + * @param array $context Context info. + */ + public function __construct(ServerRequest $request, array $context) + { + $this->_request = $request; + $context += [ + 'schema' => [], + 'required' => [], + 'defaults' => [], + 'errors' => [], + ]; + $this->_context = $context; + } + + /** + * Get the fields used in the context as a primary key. + * + * @return array + */ + public function primaryKey() + { + if (empty($this->_context['schema']['_constraints']) || + !is_array($this->_context['schema']['_constraints']) + ) { + return []; + } + foreach ($this->_context['schema']['_constraints'] as $data) { + if (isset($data['type']) && $data['type'] === 'primary') { + return isset($data['columns']) ? (array)$data['columns'] : []; + } + } + + return []; + } + + /** + * {@inheritDoc} + */ + public function isPrimaryKey($field) + { + $primaryKey = $this->primaryKey(); + + return in_array($field, $primaryKey); + } + + /** + * Returns whether or not this form is for a create operation. + * + * For this method to return true, both the primary key constraint + * must be defined in the 'schema' data, and the 'defaults' data must + * contain a value for all fields in the key. + * + * @return bool + */ + public function isCreate() + { + $primary = $this->primaryKey(); + foreach ($primary as $column) { + if (!empty($this->_context['defaults'][$column])) { + return false; + } + } + + return true; + } + + /** + * Get the current value for a given field. + * + * This method will coalesce the current request data and the 'defaults' + * array. + * + * @param string $field A dot separated path to the field a value + * is needed for. + * @param array $options Options: + * - `default`: Default value to return if no value found in request + * data or context record. + * - `schemaDefault`: Boolean indicating whether default value from + * context's schema should be used if it's not explicitly provided. + * @return mixed + */ + public function val($field, $options = []) + { + $options += [ + 'default' => null, + 'schemaDefault' => true + ]; + + $val = $this->_request->getData($field); + if ($val !== null) { + return $val; + } + if ($options['default'] !== null || !$options['schemaDefault']) { + return $options['default']; + } + if (empty($this->_context['defaults']) || !is_array($this->_context['defaults'])) { + return null; + } + + // Using Hash::check here incase the default value is actually null + if (Hash::check($this->_context['defaults'], $field)) { + return Hash::get($this->_context['defaults'], $field); + } + + return Hash::get($this->_context['defaults'], $this->stripNesting($field)); + } + + /** + * Check if a given field is 'required'. + * + * In this context class, this is simply defined by the 'required' array. + * + * @param string $field A dot separated path to check required-ness for. + * @return bool + */ + public function isRequired($field) + { + if (!is_array($this->_context['required'])) { + return false; + } + $required = Hash::get($this->_context['required'], $field); + if ($required === null) { + $required = Hash::get($this->_context['required'], $this->stripNesting($field)); + } + + return (bool)$required; + } + + /** + * {@inheritDoc} + */ + public function fieldNames() + { + $schema = $this->_context['schema']; + unset($schema['_constraints'], $schema['_indexes']); + + return array_keys($schema); + } + + /** + * Get the abstract field type for a given field name. + * + * @param string $field A dot separated path to get a schema type for. + * @return null|string An abstract data type or null. + * @see \Cake\Database\Type + */ + public function type($field) + { + if (!is_array($this->_context['schema'])) { + return null; + } + + $schema = Hash::get($this->_context['schema'], $field); + if ($schema === null) { + $schema = Hash::get($this->_context['schema'], $this->stripNesting($field)); + } + + return isset($schema['type']) ? $schema['type'] : null; + } + + /** + * Get an associative array of other attributes for a field name. + * + * @param string $field A dot separated path to get additional data on. + * @return array An array of data describing the additional attributes on a field. + */ + public function attributes($field) + { + if (!is_array($this->_context['schema'])) { + return []; + } + $schema = Hash::get($this->_context['schema'], $field); + if ($schema === null) { + $schema = Hash::get($this->_context['schema'], $this->stripNesting($field)); + } + $whitelist = ['length' => null, 'precision' => null]; + + return array_intersect_key((array)$schema, $whitelist); + } + + /** + * Check whether or not a field has an error attached to it + * + * @param string $field A dot separated path to check errors on. + * @return bool Returns true if the errors for the field are not empty. + */ + public function hasError($field) + { + if (empty($this->_context['errors'])) { + return false; + } + + return (bool)Hash::check($this->_context['errors'], $field); + } + + /** + * Get the errors for a given field + * + * @param string $field A dot separated path to check errors on. + * @return array An array of errors, an empty array will be returned when the + * context has no errors. + */ + public function error($field) + { + if (empty($this->_context['errors'])) { + return []; + } + + return Hash::get($this->_context['errors'], $field); + } + + /** + * Strips out any numeric nesting + * + * For example users.0.age will output as users.age + * + * @param string $field A dot separated path + * @return string A string with stripped numeric nesting + */ + protected function stripNesting($field) + { + return preg_replace('/\.\d*\./', '.', $field); + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/Form/ContextFactory.php b/app/vendor/cakephp/cakephp/src/View/Form/ContextFactory.php new file mode 100644 index 000000000..a3b68db7d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/Form/ContextFactory.php @@ -0,0 +1,155 @@ + 'a-string', 'callable' => ..]` + */ + public function __construct(array $providers = []) + { + foreach ($providers as $provider) { + $this->addProvider($provider['type'], $provider['callable']); + } + } + + /** + * Create factory instance with providers "array", "form" and "orm". + * + * @param array $providers Array of provider callables. Each element should + * be of form `['type' => 'a-string', 'callable' => ..]` + * @return \Cake\View\Form\ContextFactory + */ + public static function createWithDefaults(array $providers = []) + { + $providers = [ + [ + 'type' => 'orm', + 'callable' => function ($request, $data) { + if (is_array($data['entity']) || $data['entity'] instanceof Traversable) { + $pass = (new Collection($data['entity']))->first() !== null; + if ($pass) { + return new EntityContext($request, $data); + } + } + if ($data['entity'] instanceof EntityInterface) { + return new EntityContext($request, $data); + } + if (is_array($data['entity']) && empty($data['entity']['schema'])) { + return new EntityContext($request, $data); + } + } + ], + [ + 'type' => 'array', + 'callable' => function ($request, $data) { + if (is_array($data['entity']) && isset($data['entity']['schema'])) { + return new ArrayContext($request, $data['entity']); + } + } + ], + [ + 'type' => 'form', + 'callable' => function ($request, $data) { + if ($data['entity'] instanceof Form) { + return new FormContext($request, $data); + } + } + ], + ] + $providers; + + return new static($providers); + } + + /** + * Add a new context type. + * + * Form context types allow FormHelper to interact with + * data providers that come from outside CakePHP. For example + * if you wanted to use an alternative ORM like Doctrine you could + * create and connect a new context class to allow FormHelper to + * read metadata from doctrine. + * + * @param string $type The type of context. This key + * can be used to overwrite existing providers. + * @param callable $check A callable that returns an object + * when the form context is the correct type. + * @return $this + */ + public function addProvider($type, callable $check) + { + $this->providers = [$type => ['type' => $type, 'callable' => $check]] + + $this->providers; + + return $this; + } + + /** + * Find the matching context for the data. + * + * If no type can be matched a NullContext will be returned. + * + * @param \Cake\Http\ServerRequest $request Request instance. + * @param array $data The data to get a context provider for. + * @return \Cake\View\Form\ContextInterface Context provider. + * @throws \RuntimeException when the context class does not implement the + * ContextInterface. + */ + public function get(ServerRequest $request, array $data = []) + { + $data += ['entity' => null]; + + foreach ($this->providers as $provider) { + $check = $provider['callable']; + $context = $check($request, $data); + if ($context) { + break; + } + } + if (!isset($context)) { + $context = new NullContext($request, $data); + } + if (!($context instanceof ContextInterface)) { + throw new RuntimeException(sprintf( + 'Context providers must return object implementing %s. Got "%s" instead.', + ContextInterface::class, + getTypeName($context) + )); + } + + return $context; + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/Form/ContextInterface.php b/app/vendor/cakephp/cakephp/src/View/Form/ContextInterface.php new file mode 100644 index 000000000..3daa8f59c --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/Form/ContextInterface.php @@ -0,0 +1,113 @@ +validators when + * dealing with associated forms. + */ +class EntityContext implements ContextInterface +{ + use LocatorAwareTrait; + + /** + * The request object. + * + * @var \Cake\Http\ServerRequest + */ + protected $_request; + + /** + * Context data for this object. + * + * @var array + */ + protected $_context; + + /** + * The name of the top level entity/table object. + * + * @var string + */ + protected $_rootName; + + /** + * Boolean to track whether or not the entity is a + * collection. + * + * @var bool + */ + protected $_isCollection = false; + + /** + * A dictionary of tables + * + * @var array + */ + protected $_tables = []; + + /** + * Dictionary of validators. + * + * @var \Cake\Validation\Validator[] + */ + protected $_validator = []; + + /** + * Constructor. + * + * @param \Cake\Http\ServerRequest $request The request object. + * @param array $context Context info. + */ + public function __construct(ServerRequest $request, array $context) + { + $this->_request = $request; + $context += [ + 'entity' => null, + 'table' => null, + 'validator' => [], + ]; + $this->_context = $context; + $this->_prepare(); + } + + /** + * Prepare some additional data from the context. + * + * If the table option was provided to the constructor and it + * was a string, TableLocator will be used to get the correct table instance. + * + * If an object is provided as the table option, it will be used as is. + * + * If no table option is provided, the table name will be derived based on + * naming conventions. This inference will work with a number of common objects + * like arrays, Collection objects and ResultSets. + * + * @return void + * @throws \RuntimeException When a table object cannot be located/inferred. + */ + protected function _prepare() + { + $table = $this->_context['table']; + $entity = $this->_context['entity']; + if (empty($table)) { + if (is_array($entity) || $entity instanceof Traversable) { + foreach ($entity as $e) { + $entity = $e; + break; + } + } + $isEntity = $entity instanceof EntityInterface; + + if ($isEntity) { + $table = $entity->getSource(); + } + if (!$table && $isEntity && get_class($entity) !== 'Cake\ORM\Entity') { + list(, $entityClass) = namespaceSplit(get_class($entity)); + $table = Inflector::pluralize($entityClass); + } + } + if (is_string($table)) { + $table = $this->getTableLocator()->get($table); + } + + if (!($table instanceof RepositoryInterface)) { + throw new RuntimeException( + 'Unable to find table class for current entity' + ); + } + $this->_isCollection = ( + is_array($entity) || + $entity instanceof Traversable + ); + + $alias = $this->_rootName = $table->getAlias(); + $this->_tables[$alias] = $table; + } + + /** + * Get the primary key data for the context. + * + * Gets the primary key columns from the root entity's schema. + * + * @return array + */ + public function primaryKey() + { + return (array)$this->_tables[$this->_rootName]->getPrimaryKey(); + } + + /** + * {@inheritDoc} + */ + public function isPrimaryKey($field) + { + $parts = explode('.', $field); + $table = $this->_getTable($parts); + $primaryKey = (array)$table->getPrimaryKey(); + + return in_array(array_pop($parts), $primaryKey); + } + + /** + * Check whether or not this form is a create or update. + * + * If the context is for a single entity, the entity's isNew() method will + * be used. If isNew() returns null, a create operation will be assumed. + * + * If the context is for a collection or array the first object in the + * collection will be used. + * + * @return bool + */ + public function isCreate() + { + $entity = $this->_context['entity']; + if (is_array($entity) || $entity instanceof Traversable) { + foreach ($entity as $e) { + $entity = $e; + break; + } + } + if ($entity instanceof EntityInterface) { + return $entity->isNew() !== false; + } + + return true; + } + + /** + * Get the value for a given path. + * + * Traverses the entity data and finds the value for $path. + * + * @param string $field The dot separated path to the value. + * @param array $options Options: + * - `default`: Default value to return if no value found in request + * data or entity. + * - `schemaDefault`: Boolean indicating whether default value from table + * schema should be used if it's not explicitly provided. + * @return mixed The value of the field or null on a miss. + */ + public function val($field, $options = []) + { + $options += [ + 'default' => null, + 'schemaDefault' => true + ]; + + $val = $this->_request->getData($field); + if ($val !== null) { + return $val; + } + if (empty($this->_context['entity'])) { + return $options['default']; + } + $parts = explode('.', $field); + $entity = $this->entity($parts); + + if (end($parts) === '_ids' && !empty($entity)) { + return $this->_extractMultiple($entity, $parts); + } + + if ($entity instanceof EntityInterface) { + $part = array_pop($parts); + $val = $entity->get($part); + if ($val !== null) { + return $val; + } + if ($options['default'] !== null + || !$options['schemaDefault'] + || !$entity->isNew() + ) { + return $options['default']; + } + + return $this->_schemaDefault($part, $entity); + } + if (is_array($entity) || $entity instanceof ArrayAccess) { + $key = array_pop($parts); + + return isset($entity[$key]) ? $entity[$key] : $options['default']; + } + + return null; + } + + /** + * Get default value from table schema for given entity field. + * + * @param string $field Field name. + * @param \Cake\Datasource\EntityInterface $entity The entity. + * @return mixed + */ + protected function _schemaDefault($field, $entity) + { + $table = $this->_getTable($entity); + if ($table === false) { + return null; + } + $defaults = $table->getSchema()->defaultValues(); + if (!array_key_exists($field, $defaults)) { + return null; + } + + return $defaults[$field]; + } + + /** + * Helper method used to extract all the primary key values out of an array, The + * primary key column is guessed out of the provided $path array + * + * @param array|\Traversable $values The list from which to extract primary keys from + * @param array $path Each one of the parts in a path for a field name + * @return array|null + */ + protected function _extractMultiple($values, $path) + { + if (!(is_array($values) || $values instanceof Traversable)) { + return null; + } + $table = $this->_getTable($path, false); + $primary = $table ? (array)$table->getPrimaryKey() : ['id']; + + return (new Collection($values))->extract($primary[0])->toArray(); + } + + /** + * Fetch the leaf entity for the given path. + * + * This method will traverse the given path and find the leaf + * entity. If the path does not contain a leaf entity false + * will be returned. + * + * @param array|null $path Each one of the parts in a path for a field name + * or null to get the entity passed in constructor context. + * @return \Cake\Datasource\EntityInterface|\Traversable|array|bool + * @throws \RuntimeException When properties cannot be read. + */ + public function entity($path = null) + { + if ($path === null) { + return $this->_context['entity']; + } + + $oneElement = count($path) === 1; + if ($oneElement && $this->_isCollection) { + return false; + } + $entity = $this->_context['entity']; + if ($oneElement) { + return $entity; + } + + if ($path[0] === $this->_rootName) { + $path = array_slice($path, 1); + } + + $len = count($path); + $last = $len - 1; + for ($i = 0; $i < $len; $i++) { + $prop = $path[$i]; + $next = $this->_getProp($entity, $prop); + $isLast = ($i === $last); + + if (!$isLast && $next === null && $prop !== '_ids') { + $table = $this->_getTable($path); + + return $table->newEntity(); + } + + $isTraversable = ( + is_array($next) || + $next instanceof Traversable || + $next instanceof EntityInterface + ); + if ($isLast || !$isTraversable) { + return $entity; + } + $entity = $next; + } + throw new RuntimeException(sprintf( + 'Unable to fetch property "%s"', + implode('.', $path) + )); + } + + /** + * Read property values or traverse arrays/iterators. + * + * @param mixed $target The entity/array/collection to fetch $field from. + * @param string $field The next field to fetch. + * @return mixed + */ + protected function _getProp($target, $field) + { + if (is_array($target) && isset($target[$field])) { + return $target[$field]; + } + if ($target instanceof EntityInterface) { + return $target->get($field); + } + if ($target instanceof Traversable) { + foreach ($target as $i => $val) { + if ($i == $field) { + return $val; + } + } + + return false; + } + } + + /** + * Check if a field should be marked as required. + * + * @param string $field The dot separated path to the field you want to check. + * @return bool + */ + public function isRequired($field) + { + $parts = explode('.', $field); + $entity = $this->entity($parts); + + $isNew = true; + if ($entity instanceof EntityInterface) { + $isNew = $entity->isNew(); + } + + $validator = $this->_getValidator($parts); + $fieldName = array_pop($parts); + if (!$validator->hasField($fieldName)) { + return false; + } + if ($this->type($field) !== 'boolean') { + return $validator->isEmptyAllowed($fieldName, $isNew) === false; + } + + return false; + } + + /** + * Get the field names from the top level entity. + * + * If the context is for an array of entities, the 0th index will be used. + * + * @return array Array of fieldnames in the table/entity. + */ + public function fieldNames() + { + $table = $this->_getTable('0'); + + return $table->getSchema()->columns(); + } + + /** + * Get the validator associated to an entity based on naming + * conventions. + * + * @param array $parts Each one of the parts in a path for a field name + * @return \Cake\Validation\Validator + */ + protected function _getValidator($parts) + { + $keyParts = array_filter(array_slice($parts, 0, -1), function ($part) { + return !is_numeric($part); + }); + $key = implode('.', $keyParts); + $entity = $this->entity($parts) ?: null; + + if (isset($this->_validator[$key])) { + $this->_validator[$key]->setProvider('entity', $entity); + + return $this->_validator[$key]; + } + + $table = $this->_getTable($parts); + $alias = $table->getAlias(); + + $method = 'default'; + if (is_string($this->_context['validator'])) { + $method = $this->_context['validator']; + } elseif (isset($this->_context['validator'][$alias])) { + $method = $this->_context['validator'][$alias]; + } + + $validator = $table->getValidator($method); + $validator->setProvider('entity', $entity); + + return $this->_validator[$key] = $validator; + } + + /** + * Get the table instance from a property path + * + * @param array $parts Each one of the parts in a path for a field name + * @param bool $fallback Whether or not to fallback to the last found table + * when a non-existent field/property is being encountered. + * @return \Cake\ORM\Table|bool Table instance or false + */ + protected function _getTable($parts, $fallback = true) + { + if (!is_array($parts) || count($parts) === 1) { + return $this->_tables[$this->_rootName]; + } + + $normalized = array_slice(array_filter($parts, function ($part) { + return !is_numeric($part); + }), 0, -1); + + $path = implode('.', $normalized); + if (isset($this->_tables[$path])) { + return $this->_tables[$path]; + } + + if (current($normalized) === $this->_rootName) { + $normalized = array_slice($normalized, 1); + } + + $table = $this->_tables[$this->_rootName]; + $assoc = null; + foreach ($normalized as $part) { + if ($part === '_joinData') { + if ($assoc) { + $table = $assoc->junction(); + $assoc = null; + continue; + } + } else { + $assoc = $table->associations()->getByProperty($part); + } + + if (!$assoc && $fallback) { + break; + } + if (!$assoc && !$fallback) { + return false; + } + + $table = $assoc->getTarget(); + } + + return $this->_tables[$path] = $table; + } + + /** + * Get the abstract field type for a given field name. + * + * @param string $field A dot separated path to get a schema type for. + * @return null|string An abstract data type or null. + * @see \Cake\Database\Type + */ + public function type($field) + { + $parts = explode('.', $field); + $table = $this->_getTable($parts); + + return $table->getSchema()->baseColumnType(array_pop($parts)); + } + + /** + * Get an associative array of other attributes for a field name. + * + * @param string $field A dot separated path to get additional data on. + * @return array An array of data describing the additional attributes on a field. + */ + public function attributes($field) + { + $parts = explode('.', $field); + $table = $this->_getTable($parts); + $column = (array)$table->getSchema()->getColumn(array_pop($parts)); + $whitelist = ['length' => null, 'precision' => null]; + + return array_intersect_key($column, $whitelist); + } + + /** + * Check whether or not a field has an error attached to it + * + * @param string $field A dot separated path to check errors on. + * @return bool Returns true if the errors for the field are not empty. + */ + public function hasError($field) + { + return $this->error($field) !== []; + } + + /** + * Get the errors for a given field + * + * @param string $field A dot separated path to check errors on. + * @return array An array of errors. + */ + public function error($field) + { + $parts = explode('.', $field); + $entity = $this->entity($parts); + + if ($entity instanceof EntityInterface) { + return $entity->getError(array_pop($parts)); + } + + return []; + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/Form/FormContext.php b/app/vendor/cakephp/cakephp/src/View/Form/FormContext.php new file mode 100644 index 000000000..e1616014e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/Form/FormContext.php @@ -0,0 +1,181 @@ +_request = $request; + $context += [ + 'entity' => null, + ]; + $this->_form = $context['entity']; + } + + /** + * {@inheritDoc} + */ + public function primaryKey() + { + return []; + } + + /** + * {@inheritDoc} + */ + public function isPrimaryKey($field) + { + return false; + } + + /** + * {@inheritDoc} + */ + public function isCreate() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function val($field, $options = []) + { + $options += [ + 'default' => null, + 'schemaDefault' => true + ]; + + $val = $this->_request->getData($field); + if ($val !== null) { + return $val; + } + + if ($options['default'] !== null || !$options['schemaDefault']) { + return $options['default']; + } + + return $this->_schemaDefault($field); + } + + /** + * Get default value from form schema for given field. + * + * @param string $field Field name. + + * @return mixed + */ + protected function _schemaDefault($field) + { + $field = $this->_form->schema()->field($field); + if ($field) { + return $field['default']; + } + + return null; + } + + /** + * {@inheritDoc} + */ + public function isRequired($field) + { + $validator = $this->_form->getValidator(); + if (!$validator->hasField($field)) { + return false; + } + if ($this->type($field) !== 'boolean') { + return $validator->isEmptyAllowed($field, $this->isCreate()) === false; + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function fieldNames() + { + return $this->_form->schema()->fields(); + } + + /** + * {@inheritDoc} + */ + public function type($field) + { + return $this->_form->schema()->fieldType($field); + } + + /** + * {@inheritDoc} + */ + public function attributes($field) + { + $column = (array)$this->_form->schema()->field($field); + $whiteList = ['length' => null, 'precision' => null]; + + return array_intersect_key($column, $whiteList); + } + + /** + * {@inheritDoc} + */ + public function hasError($field) + { + $errors = $this->error($field); + + return count($errors) > 0; + } + + /** + * {@inheritDoc} + */ + public function error($field) + { + return array_values((array)Hash::get($this->_form->errors(), $field, [])); + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/Form/NullContext.php b/app/vendor/cakephp/cakephp/src/View/Form/NullContext.php new file mode 100644 index 000000000..f2e38b11e --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/Form/NullContext.php @@ -0,0 +1,125 @@ +_request = $request; + } + + /** + * {@inheritDoc} + */ + public function primaryKey() + { + return []; + } + + /** + * {@inheritDoc} + */ + public function isPrimaryKey($field) + { + return false; + } + + /** + * {@inheritDoc} + */ + public function isCreate() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function val($field) + { + return $this->_request->getData($field); + } + + /** + * {@inheritDoc} + */ + public function isRequired($field) + { + return false; + } + + /** + * {@inheritDoc} + */ + public function fieldNames() + { + return []; + } + + /** + * {@inheritDoc} + */ + public function type($field) + { + return null; + } + + /** + * {@inheritDoc} + */ + public function attributes($field) + { + return []; + } + + /** + * {@inheritDoc} + */ + public function hasError($field) + { + return false; + } + + /** + * {@inheritDoc} + */ + public function error($field) + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/Helper.php b/app/vendor/cakephp/cakephp/src/View/Helper.php new file mode 100644 index 000000000..9f0e9b3ff --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/Helper.php @@ -0,0 +1,273 @@ + ['type' => 'string', 'length' => 100]], + * primaryKey and validates ['field_name'] + * + * @var array + */ + public $fieldset = []; + + /** + * Holds tag templates. + * + * @var array + */ + public $tags = []; + + /** + * The View instance this helper is attached to + * + * @var \Cake\View\View + */ + protected $_View; + + /** + * Default Constructor + * + * @param \Cake\View\View $View The View this helper is being attached to. + * @param array $config Configuration settings for the helper. + */ + public function __construct(View $View, array $config = []) + { + $this->_View = $View; + $this->request = $View->request; + + $this->setConfig($config); + + if (!empty($this->helpers)) { + $this->_helperMap = $View->helpers()->normalizeArray($this->helpers); + } + + $this->initialize($config); + } + + /** + * Provide non fatal errors on missing method calls. + * + * @param string $method Method to invoke + * @param array $params Array of params for the method. + * @return void + */ + public function __call($method, $params) + { + trigger_error(sprintf('Method %1$s::%2$s does not exist', get_class($this), $method), E_USER_WARNING); + } + + /** + * Lazy loads helpers. + * + * @param string $name Name of the property being accessed. + * @return \Cake\View\Helper|null Helper instance if helper with provided name exists + */ + public function __get($name) + { + if (isset($this->_helperMap[$name]) && !isset($this->{$name})) { + $config = ['enabled' => false] + (array)$this->_helperMap[$name]['config']; + $this->{$name} = $this->_View->loadHelper($this->_helperMap[$name]['class'], $config); + + return $this->{$name}; + } + } + + /** + * Get the view instance this helper is bound to. + * + * @return \Cake\View\View The bound view instance. + */ + public function getView() + { + return $this->_View; + } + + /** + * Returns a string to be used as onclick handler for confirm dialogs. + * + * @param string $message Message to be displayed + * @param string $okCode Code to be executed after user chose 'OK' + * @param string $cancelCode Code to be executed after user chose 'Cancel' + * @param array $options Array of options + * @return string onclick JS code + */ + protected function _confirm($message, $okCode, $cancelCode = '', $options = []) + { + $message = str_replace('\\\n', '\n', json_encode($message)); + $confirm = "if (confirm({$message})) { {$okCode} } {$cancelCode}"; + // We cannot change the key here in 3.x, but the behavior is inverted in this case + $escape = isset($options['escape']) && $options['escape'] === false; + if ($escape) { + /** @var string $confirm */ + $confirm = h($confirm); + } + + return $confirm; + } + + /** + * Adds the given class to the element options + * + * @param array $options Array options/attributes to add a class to + * @param string|null $class The class name being added. + * @param string $key the key to use for class. + * @return array Array of options with $key set. + */ + public function addClass(array $options = [], $class = null, $key = 'class') + { + if (isset($options[$key]) && is_array($options[$key])) { + $options[$key][] = $class; + } elseif (isset($options[$key]) && trim($options[$key])) { + $options[$key] .= ' ' . $class; + } else { + $options[$key] = $class; + } + + return $options; + } + + /** + * Get the View callbacks this helper is interested in. + * + * By defining one of the callback methods a helper is assumed + * to be interested in the related event. + * + * Override this method if you need to add non-conventional event listeners. + * Or if you want helpers to listen to non-standard events. + * + * @return array + */ + public function implementedEvents() + { + $eventMap = [ + 'View.beforeRenderFile' => 'beforeRenderFile', + 'View.afterRenderFile' => 'afterRenderFile', + 'View.beforeRender' => 'beforeRender', + 'View.afterRender' => 'afterRender', + 'View.beforeLayout' => 'beforeLayout', + 'View.afterLayout' => 'afterLayout' + ]; + $events = []; + foreach ($eventMap as $event => $method) { + if (method_exists($this, $method)) { + $events[$event] = $method; + } + } + + return $events; + } + + /** + * Constructor hook method. + * + * Implement this method to avoid having to overwrite the constructor and call parent. + * + * @param array $config The configuration settings provided to this helper. + * @return void + */ + public function initialize(array $config) + { + } + + /** + * Returns an array that can be used to describe the internal state of this + * object. + * + * @return array + */ + public function __debugInfo() + { + return [ + 'helpers' => $this->helpers, + 'theme' => $this->theme, + 'plugin' => $this->plugin, + 'fieldset' => $this->fieldset, + 'tags' => $this->tags, + 'implementedEvents' => $this->implementedEvents(), + '_config' => $this->getConfig(), + ]; + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/Helper/BreadcrumbsHelper.php b/app/vendor/cakephp/cakephp/src/View/Helper/BreadcrumbsHelper.php new file mode 100644 index 000000000..06effa520 --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/Helper/BreadcrumbsHelper.php @@ -0,0 +1,335 @@ + [ + 'wrapper' => '{{content}}', + 'item' => '{{title}}{{separator}}', + 'itemWithoutLink' => '{{title}}{{separator}}', + 'separator' => '{{separator}}' + ] + ]; + + /** + * The crumb list. + * + * @var array + */ + protected $crumbs = []; + + /** + * Add a crumb to the end of the trail. + * + * @param string|array $title If provided as a string, it represents the title of the crumb. + * Alternatively, if you want to add multiple crumbs at once, you can provide an array, with each values being a + * single crumb. Arrays are expected to be of this form: + * - *title* The title of the crumb + * - *link* The link of the crumb. If not provided, no link will be made + * - *options* Options of the crumb. See description of params option of this method. + * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to + * Url::build() or null / empty if the crumb does not have a link. + * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will + * be rendered in (a
  • tag by default). It accepts two special keys: + * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to + * the link) + * - *templateVars*: Specific template vars in case you override the templates provided. + * @return $this + */ + public function add($title, $url = null, array $options = []) + { + if (is_array($title)) { + foreach ($title as $crumb) { + $this->crumbs[] = $crumb + ['title' => '', 'url' => null, 'options' => []]; + } + + return $this; + } + + $this->crumbs[] = compact('title', 'url', 'options'); + + return $this; + } + + /** + * Prepend a crumb to the start of the queue. + * + * @param string $title If provided as a string, it represents the title of the crumb. + * Alternatively, if you want to add multiple crumbs at once, you can provide an array, with each values being a + * single crumb. Arrays are expected to be of this form: + * - *title* The title of the crumb + * - *link* The link of the crumb. If not provided, no link will be made + * - *options* Options of the crumb. See description of params option of this method. + * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to + * Url::build() or null / empty if the crumb does not have a link. + * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will + * be rendered in (a
  • tag by default). It accepts two special keys: + * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to + * the link) + * - *templateVars*: Specific template vars in case you override the templates provided. + * @return $this + */ + public function prepend($title, $url = null, array $options = []) + { + if (is_array($title)) { + $crumbs = []; + foreach ($title as $crumb) { + $crumbs[] = $crumb + ['title' => '', 'url' => null, 'options' => []]; + } + + array_splice($this->crumbs, 0, 0, $crumbs); + + return $this; + } + + array_unshift($this->crumbs, compact('title', 'url', 'options')); + + return $this; + } + + /** + * Insert a crumb at a specific index. + * + * If the index already exists, the new crumb will be inserted, + * and the existing element will be shifted one index greater. + * If the index is out of bounds, it will throw an exception. + * + * @param int $index The index to insert at. + * @param string $title Title of the crumb. + * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to + * Url::build() or null / empty if the crumb does not have a link. + * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will + * be rendered in (a
  • tag by default). It accepts two special keys: + * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to + * the link) + * - *templateVars*: Specific template vars in case you override the templates provided. + * @return $this + * @throws \LogicException In case the index is out of bound + */ + public function insertAt($index, $title, $url = null, array $options = []) + { + if (!isset($this->crumbs[$index])) { + throw new LogicException(sprintf("No crumb could be found at index '%s'", $index)); + } + + array_splice($this->crumbs, $index, 0, [compact('title', 'url', 'options')]); + + return $this; + } + + /** + * Insert a crumb before the first matching crumb with the specified title. + * + * Finds the index of the first crumb that matches the provided class, + * and inserts the supplied callable before it. + * + * @param string $matchingTitle The title of the crumb you want to insert this one before. + * @param string $title Title of the crumb. + * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to + * Url::build() or null / empty if the crumb does not have a link. + * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will + * be rendered in (a
  • tag by default). It accepts two special keys: + * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to + * the link) + * - *templateVars*: Specific template vars in case you override the templates provided. + * @return $this + * @throws \LogicException In case the matching crumb can not be found + */ + public function insertBefore($matchingTitle, $title, $url = null, array $options = []) + { + $key = $this->findCrumb($matchingTitle); + + if ($key === null) { + throw new LogicException(sprintf("No crumb matching '%s' could be found.", $matchingTitle)); + } + + return $this->insertAt($key, $title, $url, $options); + } + + /** + * Insert a crumb after the first matching crumb with the specified title. + * + * Finds the index of the first crumb that matches the provided class, + * and inserts the supplied callable before it. + * + * @param string $matchingTitle The title of the crumb you want to insert this one after. + * @param string $title Title of the crumb. + * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to + * Url::build() or null / empty if the crumb does not have a link. + * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will + * be rendered in (a
  • tag by default). It accepts two special keys: + * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to + * the link) + * - *templateVars*: Specific template vars in case you override the templates provided. + * @return $this + * @throws \LogicException In case the matching crumb can not be found. + */ + public function insertAfter($matchingTitle, $title, $url = null, array $options = []) + { + $key = $this->findCrumb($matchingTitle); + + if ($key === null) { + throw new LogicException(sprintf("No crumb matching '%s' could be found.", $matchingTitle)); + } + + return $this->insertAt($key + 1, $title, $url, $options); + } + + /** + * Returns the crumb list. + * + * @return array + */ + public function getCrumbs() + { + return $this->crumbs; + } + + /** + * Removes all existing crumbs. + * + * @return $this + */ + public function reset() + { + $this->crumbs = []; + + return $this; + } + + /** + * Renders the breadcrumbs trail. + * + * @param array $attributes Array of attributes applied to the `wrapper` template. Accepts the `templateVars` key to + * allow the insertion of custom template variable in the template. + * @param array $separator Array of attributes for the `separator` template. + * Possible properties are : + * - *separator* The string to be displayed as a separator + * - *templateVars* Allows the insertion of custom template variable in the template + * - *innerAttrs* To provide attributes in case your separator is divided in two elements. + * All other properties will be converted as HTML attributes and will replace the *attrs* key in the template. + * If you use the default for this option (empty), it will not render a separator. + * @return string The breadcrumbs trail + */ + public function render(array $attributes = [], array $separator = []) + { + if (!$this->crumbs) { + return ''; + } + + $crumbs = $this->crumbs; + $crumbsCount = count($crumbs); + $templater = $this->templater(); + $separatorString = ''; + + if ($separator) { + if (isset($separator['innerAttrs'])) { + $separator['innerAttrs'] = $templater->formatAttributes($separator['innerAttrs']); + } + + $separator['attrs'] = $templater->formatAttributes( + $separator, + ['innerAttrs', 'separator'] + ); + + $separatorString = $this->formatTemplate('separator', $separator); + } + + $crumbTrail = ''; + foreach ($crumbs as $key => $crumb) { + $url = $crumb['url'] ? $this->Url->build($crumb['url']) : null; + $title = $crumb['title']; + $options = $crumb['options']; + + $optionsLink = []; + if (isset($options['innerAttrs'])) { + $optionsLink = $options['innerAttrs']; + unset($options['innerAttrs']); + } + + $template = 'item'; + $templateParams = [ + 'attrs' => $templater->formatAttributes($options, ['templateVars']), + 'innerAttrs' => $templater->formatAttributes($optionsLink), + 'title' => $title, + 'url' => $url, + 'separator' => '', + 'templateVars' => isset($options['templateVars']) ? $options['templateVars'] : [] + ]; + + if (!$url) { + $template = 'itemWithoutLink'; + } + + if ($separatorString && $key !== ($crumbsCount - 1)) { + $templateParams['separator'] = $separatorString; + } + + $crumbTrail .= $this->formatTemplate($template, $templateParams); + } + + $crumbTrail = $this->formatTemplate('wrapper', [ + 'content' => $crumbTrail, + 'attrs' => $templater->formatAttributes($attributes, ['templateVars']), + 'templateVars' => isset($attributes['templateVars']) ? $attributes['templateVars'] : [] + ]); + + return $crumbTrail; + } + + /** + * Search a crumb in the current stack which title matches the one provided as argument. + * If found, the index of the matching crumb will be returned. + * + * @param string $title Title to find. + * @return int|null Index of the crumb found, or null if it can not be found. + */ + protected function findCrumb($title) + { + foreach ($this->crumbs as $key => $crumb) { + if ($crumb['title'] === $title) { + return $key; + } + } + + return null; + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/Helper/FlashHelper.php b/app/vendor/cakephp/cakephp/src/View/Helper/FlashHelper.php new file mode 100644 index 000000000..555c20f5a --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/Helper/FlashHelper.php @@ -0,0 +1,104 @@ +Flash->render('somekey'); + * Will default to flash if no param is passed + * + * You can pass additional information into the flash message generation. This allows you + * to consolidate all the parameters for a given type of flash message into the view. + * + * ``` + * echo $this->Flash->render('flash', ['params' => ['name' => $user['User']['name']]]); + * ``` + * + * This would pass the current user's name into the flash message, so you could create personalized + * messages without the controller needing access to that data. + * + * Lastly you can choose the element that is used for rendering the flash message. Using + * custom elements allows you to fully customize how flash messages are generated. + * + * ``` + * echo $this->Flash->render('flash', ['element' => 'my_custom_element']); + * ``` + * + * If you want to use an element from a plugin for rendering your flash message + * you can use the dot notation for the plugin's element name: + * + * ``` + * echo $this->Flash->render('flash', [ + * 'element' => 'MyPlugin.my_custom_element', + * ]); + * ``` + * + * If you have several messages stored in the Session, each message will be rendered in its own + * element. + * + * @param string $key The [Flash.]key you are rendering in the view. + * @param array $options Additional options to use for the creation of this flash message. + * Supports the 'params', and 'element' keys that are used in the helper. + * @return string|null Rendered flash message or null if flash key does not exist + * in session. + * @throws \UnexpectedValueException If value for flash settings key is not an array. + */ + public function render($key = 'flash', array $options = []) + { + if (!$this->request->getSession()->check("Flash.$key")) { + return null; + } + + $flash = $this->request->getSession()->read("Flash.$key"); + if (!is_array($flash)) { + throw new UnexpectedValueException(sprintf( + 'Value for flash setting key "%s" must be an array.', + $key + )); + } + $this->request->getSession()->delete("Flash.$key"); + + $out = ''; + foreach ($flash as $message) { + $message = $options + $message; + $out .= $this->_View->element($message['element'], $message); + } + + return $out; + } + + /** + * Event listeners. + * + * @return array + */ + public function implementedEvents() + { + return []; + } +} diff --git a/app/vendor/cakephp/cakephp/src/View/Helper/FormHelper.php b/app/vendor/cakephp/cakephp/src/View/Helper/FormHelper.php new file mode 100644 index 000000000..c632ed77d --- /dev/null +++ b/app/vendor/cakephp/cakephp/src/View/Helper/FormHelper.php @@ -0,0 +1,2935 @@ + null, + 'errorClass' => 'form-error', + 'typeMap' => [ + 'string' => 'text', + 'text' => 'textarea', + 'uuid' => 'string', + 'datetime' => 'datetime', + 'timestamp' => 'datetime', + 'date' => 'date', + 'time' => 'time', + 'boolean' => 'checkbox', + 'float' => 'number', + 'integer' => 'number', + 'tinyinteger' => 'number', + 'smallinteger' => 'number', + 'decimal' => 'number', + 'binary' => 'file', + ], + 'templates' => [ + // Used for button elements in button(). + 'button' => '{{text}}', + // Used for checkboxes in checkbox() and multiCheckbox(). + 'checkbox' => '', + // Input group wrapper for checkboxes created via control(). + 'checkboxFormGroup' => '{{label}}', + // Wrapper container for checkboxes. + 'checkboxWrapper' => '
    {{label}}
    ', + // Widget ordering for date/time/datetime pickers. + 'dateWidget' => '{{year}}{{month}}{{day}}{{hour}}{{minute}}{{second}}{{meridian}}', + // Error message wrapper elements. + 'error' => '
    {{content}}
    ', + // Container for error items. + 'errorList' => '
      {{content}}
    ', + // Error item wrapper. + 'errorItem' => '
  • {{text}}
  • ', + // File input used by file(). + 'file' => '', + // Fieldset element used by allControls(). + 'fieldset' => '{{content}}', + // Open tag used by create(). + 'formStart' => '', + // Close tag used by end(). + 'formEnd' => '', + // General grouping container for control(). Defines input/label ordering. + 'formGroup' => '{{label}}{{input}}', + // Wrapper content used to hide other content. + 'hiddenBlock' => '
    {{content}}
    ', + // Generic input element. + 'input' => '', + // Submit input element. + 'inputSubmit' => '', + // Container element used by control(). + 'inputContainer' => '
    {{content}}
    ', + // Container element used by control() when a field has an error. + 'inputContainerError' => '
    {{content}}{{error}}
    ', + // Label element when inputs are not nested inside the label. + 'label' => '{{text}}', + // Label element used for radio and multi-checkbox inputs. + 'nestingLabel' => '{{hidden}}{{input}}{{text}}', + // Legends created by allControls() + 'legend' => '{{text}}', + // Multi-Checkbox input set title element. + 'multicheckboxTitle' => '{{text}}', + // Multi-Checkbox wrapping container. + 'multicheckboxWrapper' => '{{content}}', + // Option element used in select pickers. + 'option' => '', + // Option group element used in select pickers. + 'optgroup' => '{{content}}', + // Select element, + 'select' => '', + // Multi-select element, + 'selectMultiple' => '', + // Radio input element, + 'radio' => '', + // Wrapping container for radio input/label, + 'radioWrapper' => '{{label}}', + // Textarea input element, + 'textarea' => '', + // Container for submit buttons. + 'submitContainer' => '
    {{content}}
    ', + ] + ]; + + /** + * Default widgets + * + * @var array + */ + protected $_defaultWidgets = [ + 'button' => ['Button'], + 'checkbox' => ['Checkbox'], + 'file' => ['File'], + 'label' => ['Label'], + 'nestingLabel' => ['NestingLabel'], + 'multicheckbox' => ['MultiCheckbox', 'nestingLabel'], + 'radio' => ['Radio', 'nestingLabel'], + 'select' => ['SelectBox'], + 'textarea' => ['Textarea'], + 'datetime' => ['DateTime', 'select'], + '_default' => ['Basic'], + ]; + + /** + * List of fields created, used with secure forms. + * + * @var array + */ + public $fields = []; + + /** + * Constant used internally to skip the securing process, + * and neither add the field to the hash or to the unlocked fields. + * + * @var string + */ + const SECURE_SKIP = 'skip'; + + /** + * Defines the type of form being created. Set by FormHelper::create(). + * + * @var string|null + */ + public $requestType; + + /** + * An array of field names that have been excluded from + * the Token hash used by SecurityComponent's validatePost method + * + * @see \Cake\View\Helper\FormHelper::_secure() + * @see \Cake\Controller\Component\SecurityComponent::validatePost() + * @var array + */ + protected $_unlockedFields = []; + + /** + * Locator for input widgets. + * + * @var \Cake\View\Widget\WidgetLocator + */ + protected $_locator; + + /** + * Context for the current form. + * + * @var \Cake\View\Form\ContextInterface|null + */ + protected $_context; + + /** + * Context factory. + * + * @var \Cake\View\Form\ContextFactory + */ + protected $_contextFactory; + + /** + * The action attribute value of the last created form. + * Used to make form/request specific hashes for SecurityComponent. + * + * @var string + */ + protected $_lastAction = ''; + + /** + * The sources to be used when retrieving prefilled input values. + * + * @var array + */ + protected $_valueSources = ['context']; + + /** + * Grouped input types. + * + * @var array + */ + protected $_groupedInputTypes = ['radio', 'multicheckbox', 'date', 'time', 'datetime']; + + /** + * Construct the widgets and binds the default context providers + * + * @param \Cake\View\View $View The View this helper is being attached to. + * @param array $config Configuration settings for the helper. + */ + public function __construct(View $View, array $config = []) + { + $locator = null; + $widgets = $this->_defaultWidgets; + if (isset($config['registry'])) { + deprecationWarning('`registry` config key is deprecated in FormHelper, use `locator` instead.'); + $config['locator'] = $config['registry']; + unset($config['registry']); + } + if (isset($config['locator'])) { + $locator = $config['locator']; + unset($config['locator']); + } + if (isset($config['widgets'])) { + if (is_string($config['widgets'])) { + $config['widgets'] = (array)$config['widgets']; + } + $widgets = $config['widgets'] + $widgets; + unset($config['widgets']); + } + + if (isset($config['groupedInputTypes'])) { + $this->_groupedInputTypes = $config['groupedInputTypes']; + unset($config['groupedInputTypes']); + } + + parent::__construct($View, $config); + + if (!$locator) { + $locator = new WidgetLocator($this->templater(), $this->_View, $widgets); + } + $this->setWidgetLocator($locator); + $this->_idPrefix = $this->getConfig('idPrefix'); + } + + /** + * Set the widget registry the helper will use. + * + * @param \Cake\View\Widget\WidgetRegistry|null $instance The registry instance to set. + * @param array $widgets An array of widgets + * @return \Cake\View\Widget\WidgetRegistry + * @deprecated 3.6.0 Use FormHelper::widgetLocator() instead. + */ + public function widgetRegistry(WidgetRegistry $instance = null, $widgets = []) + { + deprecationWarning('widgetRegistry is deprecated, use widgetLocator instead.'); + + if ($instance) { + $instance->add($widgets); + $this->setWidgetLocator($instance); + } + + return $this->getWidgetLocator(); + } + + /** + * Get the widget locator currently used by the helper. + * + * @return \Cake\View\Widget\WidgetLocator Current locator instance + * @since 3.6.0 + */ + public function getWidgetLocator() + { + return $this->_locator; + } + + /** + * Set the widget locator the helper will use. + * + * @param \Cake\View\Widget\WidgetLocator $instance The locator instance to set. + * @return $this + * @since 3.6.0 + */ + public function setWidgetLocator(WidgetLocator $instance) + { + $this->_locator = $instance; + + return $this; + } + + /** + * Set the context factory the helper will use. + * + * @param \Cake\View\Form\ContextFactory|null $instance The context factory instance to set. + * @param array $contexts An array of context providers. + * @return \Cake\View\Form\ContextFactory + */ + public function contextFactory(ContextFactory $instance = null, array $contexts = []) + { + if ($instance === null) { + if ($this->_contextFactory === null) { + $this->_contextFactory = ContextFactory::createWithDefaults($contexts); + } + + return $this->_contextFactory; + } + $this->_contextFactory = $instance; + + return $this->_contextFactory; + } + + /** + * Returns an HTML form element. + * + * ### Options: + * + * - `type` Form method defaults to autodetecting based on the form context. If + * the form context's isCreate() method returns false, a PUT request will be done. + * - `method` Set the form's method attribute explicitly. + * - `action` The controller action the form submits to, (optional). Use this option if you + * don't need to change the controller from the current request's controller. Deprecated since 3.2, use `url`. + * - `url` The URL the form submits to. Can be a string or a URL array. If you use 'url' + * you should leave 'action' undefined. + * - `encoding` Set the accept-charset encoding for the form. Defaults to `Configure::read('App.encoding')` + * - `enctype` Set the form encoding explicitly. By default `type => file` will set `enctype` + * to `multipart/form-data`. + * - `templates` The templates you want to use for this form. Any templates will be merged on top of + * the already loaded templates. This option can either be a filename in /config that contains + * the templates you want to load, or an array of templates to use. + * - `context` Additional options for the context class. For example the EntityContext accepts a 'table' + * option that allows you to set the specific Table class the form should be based on. + * - `idPrefix` Prefix for generated ID attributes. + * - `valueSources` The sources that values should be read from. See FormHelper::setValueSources() + * - `templateVars` Provide template variables for the formStart template. + * + * @param mixed $context The context for which the form is being defined. + * Can be a ContextInterface instance, ORM entity, ORM resultset, or an + * array of meta data. You can use false or null to make a context-less form. + * @param array $options An array of html attributes and options. + * @return string An formatted opening FORM tag. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#Cake\View\Helper\FormHelper::create + */ + public function create($context = null, array $options = []) + { + $append = ''; + + if ($context instanceof ContextInterface) { + $this->context($context); + } else { + if (empty($options['context'])) { + $options['context'] = []; + } + $options['context']['entity'] = $context; + $context = $this->_getContext($options['context']); + unset($options['context']); + } + + $isCreate = $context->isCreate(); + + $options += [ + 'type' => $isCreate ? 'post' : 'put', + 'action' => null, + 'url' => null, + 'encoding' => strtolower(Configure::read('App.encoding')), + 'templates' => null, + 'idPrefix' => null, + 'valueSources' => null, + ]; + + if (isset($options['action'])) { + trigger_error('Using key `action` is deprecated, use `url` directly instead.', E_USER_DEPRECATED); + } + + if (isset($options['valueSources'])) { + $this->setValueSources($options['valueSources']); + unset($options['valueSources']); + } + + if ($options['idPrefix'] !== null) { + $this->_idPrefix = $options['idPrefix']; + } + $templater = $this->templater(); + + if (!empty($options['templates'])) { + $templater->push(); + $method = is_string($options['templates']) ? 'load' : 'add'; + $templater->{$method}($options['templates']); + } + unset($options['templates']); + + if ($options['action'] === false || $options['url'] === false) { + $url = $this->request->getRequestTarget(); + $action = null; + } else { + $url = $this->_formUrl($context, $options); + $action = $this->Url->build($url); + } + + $this->_lastAction($url); + unset($options['url'], $options['action'], $options['idPrefix']); + + $htmlAttributes = []; + switch (strtolower($options['type'])) { + case 'get': + $htmlAttributes['method'] = 'get'; + break; + // Set enctype for form + case 'file': + $htmlAttributes['enctype'] = 'multipart/form-data'; + $options['type'] = $isCreate ? 'post' : 'put'; + // Move on + case 'post': + // Move on + case 'put': + // Move on + case 'delete': + // Set patch method + case 'patch': + $append .= $this->hidden('_method', [ + 'name' => '_method', + 'value' => strtoupper($options['type']), + 'secure' => static::SECURE_SKIP + ]); + // Default to post method + default: + $htmlAttributes['method'] = 'post'; + } + if (isset($options['method'])) { + $htmlAttributes['method'] = strtolower($options['method']); + } + if (isset($options['enctype'])) { + $htmlAttributes['enctype'] = strtolower($options['enctype']); + } + + $this->requestType = strtolower($options['type']); + + if (!empty($options['encoding'])) { + $htmlAttributes['accept-charset'] = $options['encoding']; + } + unset($options['type'], $options['encoding']); + + $htmlAttributes += $options; + + $this->fields = []; + if ($this->requestType !== 'get') { + $append .= $this->_csrfField(); + } + + if (!empty($append)) { + $append = $templater->format('hiddenBlock', ['content' => $append]); + } + + $actionAttr = $templater->formatAttributes(['action' => $action, 'escape' => false]); + + return $this->formatTemplate('formStart', [ + 'attrs' => $templater->formatAttributes($htmlAttributes) . $actionAttr, + 'templateVars' => isset($options['templateVars']) ? $options['templateVars'] : [] + ]) . $append; + } + + /** + * Create the URL for a form based on the options. + * + * @param \Cake\View\Form\ContextInterface $context The context object to use. + * @param array $options An array of options from create() + * @return string|array The action attribute for the form. + */ + protected function _formUrl($context, $options) + { + if ($options['action'] === null && $options['url'] === null) { + return $this->request->getRequestTarget(); + } + + if (is_string($options['url']) || + (is_array($options['url']) && isset($options['url']['_name'])) + ) { + return $options['url']; + } + + if (isset($options['action']) && empty($options['url']['action'])) { + $options['url']['action'] = $options['action']; + } + + $actionDefaults = [ + 'plugin' => $this->plugin, + 'controller' => $this->request->getParam('controller'), + 'action' => $this->request->getParam('action'), + ]; + + $action = (array)$options['url'] + $actionDefaults; + + $pk = $context->primaryKey(); + if (count($pk)) { + $id = $this->getSourceValue($pk[0]); + } + if (empty($action[0]) && isset($id)) { + $action[0] = $id; + } + + return $action; + } + + /** + * Correctly store the last created form action URL. + * + * @param string|array $url The URL of the last form. + * @return void + */ + protected function _lastAction($url) + { + $action = Router::url($url, true); + $query = parse_url($action, PHP_URL_QUERY); + $query = $query ? '?' . $query : ''; + $this->_lastAction = parse_url($action, PHP_URL_PATH) . $query; + } + + /** + * Return a CSRF input if the request data is present. + * Used to secure forms in conjunction with CsrfComponent & + * SecurityComponent + * + * @return string + */ + protected function _csrfField() + { + if ($this->request->getParam('_Token.unlockedFields')) { + foreach ((array)$this->request->getParam('_Token.unlockedFields') as $unlocked) { + $this->_unlockedFields[] = $unlocked; + } + } + if (!$this->request->getParam('_csrfToken')) { + return ''; + } + + return $this->hidden('_csrfToken', [ + 'value' => $this->request->getParam('_csrfToken'), + 'secure' => static::SECURE_SKIP, + 'autocomplete' => 'off', + ]); + } + + /** + * Closes an HTML form, cleans up values set by FormHelper::create(), and writes hidden + * input fields where appropriate. + * + * Resets some parts of the state, shared among multiple FormHelper::create() calls, to defaults. + * + * @param array $secureAttributes Secure attributes which will be passed as HTML attributes + * into the hidden input elements generated for the Security Component. + * @return string A closing FORM tag. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#closing-the-form + */ + public function end(array $secureAttributes = []) + { + $out = ''; + + if ($this->requestType !== 'get' && $this->request->getParam('_Token')) { + $out .= $this->secure($this->fields, $secureAttributes); + $this->fields = []; + $this->_unlockedFields = []; + } + $out .= $this->formatTemplate('formEnd', []); + + $this->templater()->pop(); + $this->requestType = null; + $this->_context = null; + $this->_valueSources = ['context']; + $this->_idPrefix = $this->getConfig('idPrefix'); + + return $out; + } + + /** + * Generates a hidden field with a security hash based on the fields used in + * the form. + * + * If $secureAttributes is set, these HTML attributes will be merged into + * the hidden input tags generated for the Security Component. This is + * especially useful to set HTML5 attributes like 'form'. + * + * @param array $fields If set specifies the list of fields to use when + * generating the hash, else $this->fields is being used. + * @param array $secureAttributes will be passed as HTML attributes into the hidden + * input elements generated for the Security Component. + * @return string A hidden input field with a security hash, or empty string when + * secured forms are not in use. + */ + public function secure(array $fields = [], array $secureAttributes = []) + { + if (!$this->request->getParam('_Token')) { + return ''; + } + $debugSecurity = Configure::read('debug'); + if (isset($secureAttributes['debugSecurity'])) { + $debugSecurity = $debugSecurity && $secureAttributes['debugSecurity']; + unset($secureAttributes['debugSecurity']); + } + $secureAttributes['secure'] = static::SECURE_SKIP; + $secureAttributes['autocomplete'] = 'off'; + + $tokenData = $this->_buildFieldToken( + $this->_lastAction, + $fields, + $this->_unlockedFields + ); + $tokenFields = array_merge($secureAttributes, [ + 'value' => $tokenData['fields'], + ]); + $out = $this->hidden('_Token.fields', $tokenFields); + $tokenUnlocked = array_merge($secureAttributes, [ + 'value' => $tokenData['unlocked'], + ]); + $out .= $this->hidden('_Token.unlocked', $tokenUnlocked); + if ($debugSecurity) { + $tokenDebug = array_merge($secureAttributes, [ + 'value' => urlencode(json_encode([ + $this->_lastAction, + $fields, + $this->_unlockedFields + ])), + ]); + $out .= $this->hidden('_Token.debug', $tokenDebug); + } + + return $this->formatTemplate('hiddenBlock', ['content' => $out]); + } + + /** + * Add to or get the list of fields that are currently unlocked. + * Unlocked fields are not included in the field hash used by SecurityComponent + * unlocking a field once its been added to the list of secured fields will remove + * it from the list of fields. + * + * @param string|null $name The dot separated name for the field. + * @return array|null Either null, or the list of fields. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#working-with-securitycomponent + */ + public function unlockField($name = null) + { + if ($name === null) { + return $this->_unlockedFields; + } + if (!in_array($name, $this->_unlockedFields)) { + $this->_unlockedFields[] = $name; + } + $index = array_search($name, $this->fields); + if ($index !== false) { + unset($this->fields[$index]); + } + unset($this->fields[$name]); + } + + /** + * Determine which fields of a form should be used for hash. + * Populates $this->fields + * + * @param bool $lock Whether this field should be part of the validation + * or excluded as part of the unlockedFields. + * @param string|array $field Reference to field to be secured. Can be dot + * separated string to indicate nesting or array of fieldname parts. + * @param mixed $value Field value, if value should not be tampered with. + * @return void + */ + protected function _secure($lock, $field, $value = null) + { + if (empty($field) && $field !== '0') { + return; + } + + if (is_string($field)) { + $field = Hash::filter(explode('.', $field)); + } + + foreach ($this->_unlockedFields as $unlockField) { + $unlockParts = explode('.', $unlockField); + if (array_values(array_intersect($field, $unlockParts)) === $unlockParts) { + return; + } + } + + $field = implode('.', $field); + $field = preg_replace('/(\.\d+)+$/', '', $field); + + if ($lock) { + if (!in_array($field, $this->fields)) { + if ($value !== null) { + $this->fields[$field] = $value; + + return; + } + if (isset($this->fields[$field]) && $value === null) { + unset($this->fields[$field]); + } + $this->fields[] = $field; + } + } else { + $this->unlockField($field); + } + } + + /** + * Returns true if there is an error for the given field, otherwise false + * + * @param string $field This should be "modelname.fieldname" + * @return bool If there are errors this method returns true, else false. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#displaying-and-checking-errors + */ + public function isFieldError($field) + { + return $this->_getContext()->hasError($field); + } + + /** + * Returns a formatted error message for given form field, '' if no errors. + * + * Uses the `error`, `errorList` and `errorItem` templates. The `errorList` and + * `errorItem` templates are used to format multiple error messages per field. + * + * ### Options: + * + * - `escape` boolean - Whether or not to html escape the contents of the error. + * + * @param string $field A field name, like "modelname.fieldname" + * @param string|array|null $text Error message as string or array of messages. If an array, + * it should be a hash of key names => messages. + * @param array $options See above. + * @return string Formatted errors or ''. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#displaying-and-checking-errors + */ + public function error($field, $text = null, array $options = []) + { + if (substr($field, -5) === '._ids') { + $field = substr($field, 0, -5); + } + $options += ['escape' => true]; + + $context = $this->_getContext(); + if (!$context->hasError($field)) { + return ''; + } + $error = (array)$context->error($field); + + if (is_array($text)) { + $tmp = []; + foreach ($error as $k => $e) { + if (isset($text[$k])) { + $tmp[] = $text[$k]; + } elseif (isset($text[$e])) { + $tmp[] = $text[$e]; + } else { + $tmp[] = $e; + } + } + $text = $tmp; + } + + if ($text !== null) { + $error = $text; + } + + if ($options['escape']) { + $error = h($error); + unset($options['escape']); + } + + if (is_array($error)) { + if (count($error) > 1) { + $errorText = []; + foreach ($error as $err) { + $errorText[] = $this->formatTemplate('errorItem', ['text' => $err]); + } + $error = $this->formatTemplate('errorList', [ + 'content' => implode('', $errorText) + ]); + } else { + $error = array_pop($error); + } + } + + return $this->formatTemplate('error', ['content' => $error]); + } + + /** + * Returns a formatted LABEL element for HTML forms. + * + * Will automatically generate a `for` attribute if one is not provided. + * + * ### Options + * + * - `for` - Set the for attribute, if its not defined the for attribute + * will be generated from the $fieldName parameter using + * FormHelper::_domId(). + * + * Examples: + * + * The text and for attribute are generated off of the fieldname + * + * ``` + * echo $this->Form->label('published'); + * + * ``` + * + * Custom text: + * + * ``` + * echo $this->Form->label('published', 'Publish'); + * + * ``` + * + * Custom attributes: + * + * ``` + * echo $this->Form->label('published', 'Publish', [ + * 'for' => 'post-publish' + * ]); + * + * ``` + * + * Nesting an input tag: + * + * ``` + * echo $this->Form->label('published', 'Publish', [ + * 'for' => 'published', + * 'input' => $this->text('published'), + * ]); + * + * ``` + * + * If you want to nest inputs in the labels, you will need to modify the default templates. + * + * @param string $fieldName This should be "modelname.fieldname" + * @param string|null $text Text that will appear in the label field. If + * $text is left undefined the text will be inflected from the + * fieldName. + * @param array $options An array of HTML attributes. + * @return string The formatted LABEL element + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-labels + */ + public function label($fieldName, $text = null, array $options = []) + { + if ($text === null) { + $text = $fieldName; + if (substr($text, -5) === '._ids') { + $text = substr($text, 0, -5); + } + if (strpos($text, '.') !== false) { + $fieldElements = explode('.', $text); + $text = array_pop($fieldElements); + } + if (substr($text, -3) === '_id') { + $text = substr($text, 0, -3); + } + $text = __(Inflector::humanize(Inflector::underscore($text))); + } + + if (isset($options['for'])) { + $labelFor = $options['for']; + unset($options['for']); + } else { + $labelFor = $this->_domId($fieldName); + } + $attrs = $options + [ + 'for' => $labelFor, + 'text' => $text, + ]; + if (isset($options['input'])) { + if (is_array($options['input'])) { + $attrs = $options['input'] + $attrs; + } + + return $this->widget('nestingLabel', $attrs); + } + + return $this->widget('label', $attrs); + } + + /** + * Generate a set of controls for `$fields`. If $fields is empty the fields + * of current model will be used. + * + * You can customize individual controls through `$fields`. + * ``` + * $this->Form->allControls([ + * 'name' => ['label' => 'custom label'] + * ]); + * ``` + * + * You can exclude fields by specifying them as `false`: + * + * ``` + * $this->Form->allControls(['title' => false]); + * ``` + * + * In the above example, no field would be generated for the title field. + * + * @param array $fields An array of customizations for the fields that will be + * generated. This array allows you to set custom types, labels, or other options. + * @param array $options Options array. Valid keys are: + * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be + * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will + * be enabled + * - `legend` Set to false to disable the legend for the generated control set. Or supply a string + * to customize the legend text. + * @return string Completed form controls. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#generating-entire-forms + */ + public function allControls(array $fields = [], array $options = []) + { + $context = $this->_getContext(); + + $modelFields = $context->fieldNames(); + + $fields = array_merge( + Hash::normalize($modelFields), + Hash::normalize($fields) + ); + + return $this->controls($fields, $options); + } + + /** + * Generate a set of controls for `$fields`. If $fields is empty the fields + * of current model will be used. + * + * @param array $fields An array of customizations for the fields that will be + * generated. This array allows you to set custom types, labels, or other options. + * @param array $options Options array. Valid keys are: + * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be + * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will + * be enabled + * - `legend` Set to false to disable the legend for the generated control set. Or supply a string + * to customize the legend text. + * @return string Completed form controls. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#generating-entire-forms + * @deprecated 3.4.0 Use FormHelper::allControls() instead. + */ + public function allInputs(array $fields = [], array $options = []) + { + deprecationWarning( + 'FormHelper::allInputs() is deprecated. ' . + 'Use FormHelper::allControls() instead.' + ); + + return $this->allControls($fields, $options); + } + + /** + * Generate a set of controls for `$fields` wrapped in a fieldset element. + * + * You can customize individual controls through `$fields`. + * ``` + * $this->Form->controls([ + * 'name' => ['label' => 'custom label'], + * 'email' + * ]); + * ``` + * + * @param array $fields An array of the fields to generate. This array allows + * you to set custom types, labels, or other options. + * @param array $options Options array. Valid keys are: + * - `fieldset` Set to false to disable the fieldset. You can also pass an + * array of params to be applied as HTML attributes to the fieldset tag. + * If you pass an empty array, the fieldset will be enabled. + * - `legend` Set to false to disable the legend for the generated input set. + * Or supply a string to customize the legend text. + * @return string Completed form inputs. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#generating-entire-forms + */ + public function controls(array $fields, array $options = []) + { + $fields = Hash::normalize($fields); + + $out = ''; + foreach ($fields as $name => $opts) { + if ($opts === false) { + continue; + } + + $out .= $this->control($name, (array)$opts); + } + + return $this->fieldset($out, $options); + } + + /** + * Generate a set of controls for `$fields` wrapped in a fieldset element. + * + * @param array $fields An array of the fields to generate. This array allows + * you to set custom types, labels, or other options. + * @param array $options Options array. Valid keys are: + * - `fieldset` Set to false to disable the fieldset. You can also pass an + * array of params to be applied as HTML attributes to the fieldset tag. + * If you pass an empty array, the fieldset will be enabled. + * - `legend` Set to false to disable the legend for the generated input set. + * Or supply a string to customize the legend text. + * @return string Completed form inputs. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#generating-entire-forms + * @deprecated 3.4.0 Use FormHelper::controls() instead. + */ + public function inputs(array $fields, array $options = []) + { + deprecationWarning( + 'FormHelper::inputs() is deprecated. ' . + 'Use FormHelper::controls() instead.' + ); + + return $this->controls($fields, $options); + } + + /** + * Wrap a set of inputs in a fieldset + * + * @param string $fields the form inputs to wrap in a fieldset + * @param array $options Options array. Valid keys are: + * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be + * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will + * be enabled + * - `legend` Set to false to disable the legend for the generated input set. Or supply a string + * to customize the legend text. + * @return string Completed form inputs. + */ + public function fieldset($fields = '', array $options = []) + { + $fieldset = $legend = true; + $context = $this->_getContext(); + $out = $fields; + + if (isset($options['legend'])) { + $legend = $options['legend']; + } + if (isset($options['fieldset'])) { + $fieldset = $options['fieldset']; + } + + if ($legend === true) { + $isCreate = $context->isCreate(); + $modelName = Inflector::humanize(Inflector::singularize($this->request->getParam('controller'))); + if (!$isCreate) { + $legend = __d('cake', 'Edit {0}', $modelName); + } else { + $legend = __d('cake', 'New {0}', $modelName); + } + } + + if ($fieldset !== false) { + if ($legend) { + $out = $this->formatTemplate('legend', ['text' => $legend]) . $out; + } + + $fieldsetParams = ['content' => $out, 'attrs' => '']; + if (is_array($fieldset) && !empty($fieldset)) { + $fieldsetParams['attrs'] = $this->templater()->formatAttributes($fieldset); + } + $out = $this->formatTemplate('fieldset', $fieldsetParams); + } + + return $out; + } + + /** + * Generates a form control element complete with label and wrapper div. + * + * ### Options + * + * See each field type method for more information. Any options that are part of + * $attributes or $options for the different **type** methods can be included in `$options` for input(). + * Additionally, any unknown keys that are not in the list below, or part of the selected type's options + * will be treated as a regular HTML attribute for the generated input. + * + * - `type` - Force the type of widget you want. e.g. `type => 'select'` + * - `label` - Either a string label, or an array of options for the label. See FormHelper::label(). + * - `options` - For widgets that take options e.g. radio, select. + * - `error` - Control the error message that is produced. Set to `false` to disable any kind of error reporting (field + * error and error messages). + * - `empty` - String or boolean to enable empty select box options. + * - `nestedInput` - Used with checkbox and radio inputs. Set to false to render inputs outside of label + * elements. Can be set to true on any input to force the input inside the label. If you + * enable this option for radio buttons you will also need to modify the default `radioWrapper` template. + * - `templates` - The templates you want to use for this input. Any templates will be merged on top of + * the already loaded templates. This option can either be a filename in /config that contains + * the templates you want to load, or an array of templates to use. + * - `labelOptions` - Either `false` to disable label around nestedWidgets e.g. radio, multicheckbox or an array + * of attributes for the label tag. `selected` will be added to any classes e.g. `class => 'myclass'` where + * widget is checked + * + * @param string $fieldName This should be "modelname.fieldname" + * @param array $options Each type of input takes different options. + * @return string Completed form widget. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-form-inputs + */ + public function control($fieldName, array $options = []) + { + $options += [ + 'type' => null, + 'label' => null, + 'error' => null, + 'required' => null, + 'options' => null, + 'templates' => [], + 'templateVars' => [], + 'labelOptions' => true + ]; + $options = $this->_parseOptions($fieldName, $options); + $options += ['id' => $this->_domId($fieldName)]; + + $templater = $this->templater(); + $newTemplates = $options['templates']; + + if ($newTemplates) { + $templater->push(); + $templateMethod = is_string($options['templates']) ? 'load' : 'add'; + $templater->{$templateMethod}($options['templates']); + } + unset($options['templates']); + + $error = null; + $errorSuffix = ''; + if ($options['type'] !== 'hidden' && $options['error'] !== false) { + if (is_array($options['error'])) { + $error = $this->error($fieldName, $options['error'], $options['error']); + } else { + $error = $this->error($fieldName, $options['error']); + } + $errorSuffix = empty($error) ? '' : 'Error'; + unset($options['error']); + } + + $label = $options['label']; + unset($options['label']); + + $labelOptions = $options['labelOptions']; + unset($options['labelOptions']); + + $nestedInput = false; + if ($options['type'] === 'checkbox') { + $nestedInput = true; + } + $nestedInput = isset($options['nestedInput']) ? $options['nestedInput'] : $nestedInput; + unset($options['nestedInput']); + + if ($nestedInput === true && $options['type'] === 'checkbox' && !array_key_exists('hiddenField', $options) && $label !== false) { + $options['hiddenField'] = '_split'; + } + + $input = $this->_getInput($fieldName, $options + ['labelOptions' => $labelOptions]); + if ($options['type'] === 'hidden' || $options['type'] === 'submit') { + if ($newTemplates) { + $templater->pop(); + } + + return $input; + } + + $label = $this->_getLabel($fieldName, compact('input', 'label', 'error', 'nestedInput') + $options); + if ($nestedInput) { + $result = $this->_groupTemplate(compact('label', 'error', 'options')); + } else { + $result = $this->_groupTemplate(compact('input', 'label', 'error', 'options')); + } + $result = $this->_inputContainerTemplate([ + 'content' => $result, + 'error' => $error, + 'errorSuffix' => $errorSuffix, + 'options' => $options + ]); + + if ($newTemplates) { + $templater->pop(); + } + + return $result; + } + + /** + * Generates a form control element complete with label and wrapper div. + * + * @param string $fieldName This should be "modelname.fieldname" + * @param array $options Each type of input takes different options. + * @return string Completed form widget. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-form-inputs + * @deprecated 3.4.0 Use FormHelper::control() instead. + */ + public function input($fieldName, array $options = []) + { + deprecationWarning( + 'FormHelper::input() is deprecated. ' . + 'Use FormHelper::control() instead.' + ); + + return $this->control($fieldName, $options); + } + + /** + * Generates an group template element + * + * @param array $options The options for group template + * @return string The generated group template + */ + protected function _groupTemplate($options) + { + $groupTemplate = $options['options']['type'] . 'FormGroup'; + if (!$this->templater()->get($groupTemplate)) { + $groupTemplate = 'formGroup'; + } + + return $this->formatTemplate($groupTemplate, [ + 'input' => isset($options['input']) ? $options['input'] : [], + 'label' => $options['label'], + 'error' => $options['error'], + 'templateVars' => isset($options['options']['templateVars']) ? $options['options']['templateVars'] : [] + ]); + } + + /** + * Generates an input container template + * + * @param array $options The options for input container template + * @return string The generated input container template + */ + protected function _inputContainerTemplate($options) + { + $inputContainerTemplate = $options['options']['type'] . 'Container' . $options['errorSuffix']; + if (!$this->templater()->get($inputContainerTemplate)) { + $inputContainerTemplate = 'inputContainer' . $options['errorSuffix']; + } + + return $this->formatTemplate($inputContainerTemplate, [ + 'content' => $options['content'], + 'error' => $options['error'], + 'required' => $options['options']['required'] ? ' required' : '', + 'type' => $options['options']['type'], + 'templateVars' => isset($options['options']['templateVars']) ? $options['options']['templateVars'] : [] + ]); + } + + /** + * Generates an input element + * + * @param string $fieldName the field name + * @param array $options The options for the input element + * @return string The generated input element + */ + protected function _getInput($fieldName, $options) + { + $label = $options['labelOptions']; + unset($options['labelOptions']); + switch (strtolower($options['type'])) { + case 'select': + $opts = $options['options']; + unset($options['options']); + + return $this->select($fieldName, $opts, $options + ['label' => $label]); + case 'radio': + $opts = $options['options']; + unset($options['options']); + + return $this->radio($fieldName, $opts, $options + ['label' => $label]); + case 'multicheckbox': + $opts = $options['options']; + unset($options['options']); + + return $this->multiCheckbox($fieldName, $opts, $options + ['label' => $label]); + case 'input': + throw new RuntimeException("Invalid type 'input' used for field '$fieldName'"); + + default: + return $this->{$options['type']}($fieldName, $options); + } + } + + /** + * Generates input options array + * + * @param string $fieldName The name of the field to parse options for. + * @param array $options Options list. + * @return array Options + */ + protected function _parseOptions($fieldName, $options) + { + $needsMagicType = false; + if (empty($options['type'])) { + $needsMagicType = true; + $options['type'] = $this->_inputType($fieldName, $options); + } + + $options = $this->_magicOptions($fieldName, $options, $needsMagicType); + + return $options; + } + + /** + * Returns the input type that was guessed for the provided fieldName, + * based on the internal type it is associated too, its name and the + * variables that can be found in the view template + * + * @param string $fieldName the name of the field to guess a type for + * @param array $options the options passed to the input method + * @return string + */ + protected function _inputType($fieldName, $options) + { + $context = $this->_getContext(); + + if ($context->isPrimaryKey($fieldName)) { + return 'hidden'; + } + + if (substr($fieldName, -3) === '_id') { + return 'select'; + } + + $internalType = $context->type($fieldName); + $map = $this->_config['typeMap']; + $type = isset($map[$internalType]) ? $map[$internalType] : 'text'; + $fieldName = array_slice(explode('.', $fieldName), -1)[0]; + + switch (true) { + case isset($options['checked']): + return 'checkbox'; + case isset($options['options']): + return 'select'; + case in_array($fieldName, ['passwd', 'password']): + return 'password'; + case in_array($fieldName, ['tel', 'telephone', 'phone']): + return 'tel'; + case $fieldName === 'email': + return 'email'; + case isset($options['rows']) || isset($options['cols']): + return 'textarea'; + } + + return $type; + } + + /** + * Selects the variable containing the options for a select field if present, + * and sets the value to the 'options' key in the options array. + * + * @param string $fieldName The name of the field to find options for. + * @param array $options Options list. + * @return array + */ + protected function _optionsOptions($fieldName, $options) + { + if (isset($options['options'])) { + return $options; + } + + $pluralize = true; + if (substr($fieldName, -5) === '._ids') { + $fieldName = substr($fieldName, 0, -5); + $pluralize = false; + } elseif (substr($fieldName, -3) === '_id') { + $fieldName = substr($fieldName, 0, -3); + } + $fieldName = array_slice(explode('.', $fieldName), -1)[0]; + + $varName = Inflector::variable( + $pluralize ? Inflector::pluralize($fieldName) : $fieldName + ); + $varOptions = $this->_View->get($varName); + if (!is_array($varOptions) && !($varOptions instanceof Traversable)) { + return $options; + } + if ($options['type'] !== 'radio') { + $options['type'] = 'select'; + } + $options['options'] = $varOptions; + + return $options; + } + + /** + * Magically set option type and corresponding options + * + * @param string $fieldName The name of the field to generate options for. + * @param array $options Options list. + * @param bool $allowOverride Whether or not it is allowed for this method to + * overwrite the 'type' key in options. + * @return array + */ + protected function _magicOptions($fieldName, $options, $allowOverride) + { + $context = $this->_getContext(); + + if (!isset($options['required']) && $options['type'] !== 'hidden') { + $options['required'] = $context->isRequired($fieldName); + } + + $type = $context->type($fieldName); + $fieldDef = $context->attributes($fieldName); + + if ($options['type'] === 'number' && !isset($options['step'])) { + if ($type === 'decimal' && isset($fieldDef['precision'])) { + $decimalPlaces = $fieldDef['precision']; + $options['step'] = sprintf('%.' . $decimalPlaces . 'F', pow(10, -1 * $decimalPlaces)); + } elseif ($type === 'float') { + $options['step'] = 'any'; + } + } + + $typesWithOptions = ['text', 'number', 'radio', 'select']; + $magicOptions = (in_array($options['type'], ['radio', 'select']) || $allowOverride); + if ($magicOptions && in_array($options['type'], $typesWithOptions)) { + $options = $this->_optionsOptions($fieldName, $options); + } + + if ($allowOverride && substr($fieldName, -5) === '._ids') { + $options['type'] = 'select'; + if (!isset($options['multiple']) || ($options['multiple'] && $options['multiple'] != 'checkbox')) { + $options['multiple'] = true; + } + } + + if ($options['type'] === 'select' && array_key_exists('step', $options)) { + unset($options['step']); + } + + $autoLength = !array_key_exists('maxlength', $options) + && !empty($fieldDef['length']) + && $options['type'] !== 'select'; + + $allowedTypes = ['text', 'textarea', 'email', 'tel', 'url', 'search']; + if ($autoLength && in_array($options['type'], $allowedTypes)) { + $options['maxlength'] = min($fieldDef['length'], 100000); + } + + if (in_array($options['type'], ['datetime', 'date', 'time', 'select'])) { + $options += ['empty' => false]; + } + + return $options; + } + + /** + * Generate label for input + * + * @param string $fieldName The name of the field to generate label for. + * @param array $options Options list. + * @return bool|string false or Generated label element + */ + protected function _getLabel($fieldName, $options) + { + if ($options['type'] === 'hidden') { + return false; + } + + $label = null; + if (isset($options['label'])) { + $label = $options['label']; + } + + if ($label === false && $options['type'] === 'checkbox') { + return $options['input']; + } + if ($label === false) { + return false; + } + + return $this->_inputLabel($fieldName, $label, $options); + } + + /** + * Extracts a single option from an options array. + * + * @param string $name The name of the option to pull out. + * @param array $options The array of options you want to extract. + * @param mixed $default The default option value + * @return mixed the contents of the option or default + */ + protected function _extractOption($name, $options, $default = null) + { + if (array_key_exists($name, $options)) { + return $options[$name]; + } + + return $default; + } + + /** + * Generate a label for an input() call. + * + * $options can contain a hash of id overrides. These overrides will be + * used instead of the generated values if present. + * + * @param string $fieldName The name of the field to generate label for. + * @param string $label Label text. + * @param array $options Options for the label element. + * @return string Generated label element + */ + protected function _inputLabel($fieldName, $label, $options) + { + $options += ['id' => null, 'input' => null, 'nestedInput' => false, 'templateVars' => []]; + $labelAttributes = ['templateVars' => $options['templateVars']]; + if (is_array($label)) { + $labelText = null; + if (isset($label['text'])) { + $labelText = $label['text']; + unset($label['text']); + } + $labelAttributes = array_merge($labelAttributes, $label); + } else { + $labelText = $label; + } + + $labelAttributes['for'] = $options['id']; + if (in_array($options['type'], $this->_groupedInputTypes, true)) { + $labelAttributes['for'] = false; + } + if ($options['nestedInput']) { + $labelAttributes['input'] = $options['input']; + } + if (isset($options['escape'])) { + $labelAttributes['escape'] = $options['escape']; + } + + return $this->label($fieldName, $labelText, $labelAttributes); + } + + /** + * Creates a checkbox input widget. + * + * ### Options: + * + * - `value` - the value of the checkbox + * - `checked` - boolean indicate that this checkbox is checked. + * - `hiddenField` - boolean to indicate if you want the results of checkbox() to include + * a hidden input with a value of ''. + * - `disabled` - create a disabled input. + * - `default` - Set the default value for the checkbox. This allows you to start checkboxes + * as checked, without having to check the POST data. A matching POST data value, will overwrite + * the default value. + * + * @param string $fieldName Name of a field, like this "modelname.fieldname" + * @param array $options Array of HTML attributes. + * @return string|array An HTML text input element. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-checkboxes + */ + public function checkbox($fieldName, array $options = []) + { + $options += ['hiddenField' => true, 'value' => 1]; + + // Work around value=>val translations. + $value = $options['value']; + unset($options['value']); + $options = $this->_initInputField($fieldName, $options); + $options['value'] = $value; + + $output = ''; + if ($options['hiddenField']) { + $hiddenOptions = [ + 'name' => $options['name'], + 'value' => $options['hiddenField'] !== true && $options['hiddenField'] !== '_split' ? $options['hiddenField'] : '0', + 'form' => isset($options['form']) ? $options['form'] : null, + 'secure' => false + ]; + if (isset($options['disabled']) && $options['disabled']) { + $hiddenOptions['disabled'] = 'disabled'; + } + $output = $this->hidden($fieldName, $hiddenOptions); + } + + if ($options['hiddenField'] === '_split') { + unset($options['hiddenField'], $options['type']); + + return ['hidden' => $output, 'input' => $this->widget('checkbox', $options)]; + } + unset($options['hiddenField'], $options['type']); + + return $output . $this->widget('checkbox', $options); + } + + /** + * Creates a set of radio widgets. + * + * ### Attributes: + * + * - `value` - Indicates the value when this radio button is checked. + * - `label` - Either `false` to disable label around the widget or an array of attributes for + * the label tag. `selected` will be added to any classes e.g. `'class' => 'myclass'` where widget + * is checked + * - `hiddenField` - boolean to indicate if you want the results of radio() to include + * a hidden input with a value of ''. This is useful for creating radio sets that are non-continuous. + * - `disabled` - Set to `true` or `disabled` to disable all the radio buttons. Use an array of + * values to disable specific radio buttons. + * - `empty` - Set to `true` to create an input with the value '' as the first option. When `true` + * the radio label will be 'empty'. Set this option to a string to control the label value. + * + * @param string $fieldName Name of a field, like this "modelname.fieldname" + * @param array|\Traversable $options Radio button options array. + * @param array $attributes Array of attributes. + * @return string Completed radio widget set. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-radio-buttons + */ + public function radio($fieldName, $options = [], array $attributes = []) + { + $attributes['options'] = $options; + $attributes['idPrefix'] = $this->_idPrefix; + $attributes = $this->_initInputField($fieldName, $attributes); + + $hiddenField = isset($attributes['hiddenField']) ? $attributes['hiddenField'] : true; + unset($attributes['hiddenField']); + + $radio = $this->widget('radio', $attributes); + + $hidden = ''; + if ($hiddenField) { + $hidden = $this->hidden($fieldName, [ + 'value' => $hiddenField === true ? '' : $hiddenField, + 'form' => isset($attributes['form']) ? $attributes['form'] : null, + 'name' => $attributes['name'], + ]); + } + + return $hidden . $radio; + } + + /** + * Missing method handler - implements various simple input types. Is used to create inputs + * of various types. e.g. `$this->Form->text();` will create `` while + * `$this->Form->range();` will create `` + * + * ### Usage + * + * ``` + * $this->Form->search('User.query', ['value' => 'test']); + * ``` + * + * Will make an input like: + * + * `` + * + * The first argument to an input type should always be the fieldname, in `Model.field` format. + * The second argument should always be an array of attributes for the input. + * + * @param string $method Method name / input type to make. + * @param array $params Parameters for the method call + * @return string Formatted input method. + * @throws \Cake\Core\Exception\Exception When there are no params for the method call. + */ + public function __call($method, $params) + { + $options = []; + if (empty($params)) { + throw new Exception(sprintf('Missing field name for FormHelper::%s', $method)); + } + if (isset($params[1])) { + $options = $params[1]; + } + if (!isset($options['type'])) { + $options['type'] = $method; + } + $options = $this->_initInputField($params[0], $options); + + return $this->widget($options['type'], $options); + } + + /** + * Creates a textarea widget. + * + * ### Options: + * + * - `escape` - Whether or not the contents of the textarea should be escaped. Defaults to true. + * + * @param string $fieldName Name of a field, in the form "modelname.fieldname" + * @param array $options Array of HTML attributes, and special options above. + * @return string A generated HTML text input element + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-textareas + */ + public function textarea($fieldName, array $options = []) + { + $options = $this->_initInputField($fieldName, $options); + unset($options['type']); + + return $this->widget('textarea', $options); + } + + /** + * Creates a hidden input field. + * + * @param string $fieldName Name of a field, in the form of "modelname.fieldname" + * @param array $options Array of HTML attributes. + * @return string A generated hidden input + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-hidden-inputs + */ + public function hidden($fieldName, array $options = []) + { + $options += ['required' => false, 'secure' => true]; + + $secure = $options['secure']; + unset($options['secure']); + + $options = $this->_initInputField($fieldName, array_merge( + $options, + ['secure' => static::SECURE_SKIP] + )); + + if ($secure === true) { + $this->_secure(true, $this->_secureFieldName($options['name']), (string)$options['val']); + } + + $options['type'] = 'hidden'; + + return $this->widget('hidden', $options); + } + + /** + * Creates file input widget. + * + * @param string $fieldName Name of a field, in the form "modelname.fieldname" + * @param array $options Array of HTML attributes. + * @return string A generated file input. + * @link https://book.cakephp.org/3.0/en/views/helpers/form.html#creating-file-inputs + */ + public function file($fieldName, array $options = []) + { + $options += ['secure' => true]; + $options = $this->_initInputField($fieldName, $options); + + unset($options['type']); + + return $this->widget('file', $options); + } + + /** + * Creates a `